乐趣区

关于vue.js:从vue源码中学习观察者模式

摘要:源码解读设计模式系列文章将陆陆续续进行更新中 ~
摘要:源码解读设计模式系列文章将陆陆续续进行更新中 ~

观察者模式

首先话题下来,咱们得反诘一下本人,什么是观察者模式?

概念

观察者模式(Observer):通常又被称作为公布 - 订阅者模式。它定义了一种一对多的依赖关系,即当一个对象的状态产生扭转的时候,所有依赖于它的对象都会失去告诉并自动更新,解决了主体对象与观察者之间性能的耦合。

讲个故事

下面对于观察者模式的概念可能会比拟官网化,所以咱们讲个故事来了解它。

  • A:是密探,代号 001(发布者)
  • B:是通信人员,负责与 A 进行机密交接(订阅者)
  • A 日常工作就是在明面采集一些情报
  • B 则负责暗中察看着 A
  • 一旦 A 传递出一些无关音讯(更多时候须要对音讯进行封装传递,前面依据源码具体分析)
  • B 会立马订阅到该音讯,而后做一些绝对应的变更,比如说告诉做一些事件应答的一些动作。

适用性

以下任一场景都能够应用观察者模式

  1. 当一个形象模型有两个方面,其中一个方面依赖于另一方面。讲这两者封装在独立的对象中能够让它们能够各自独立的扭转和复用
  2. 当一个对象的扭转的时候,须要同时扭转其它对象,然而却不晓得具体多少对象有待扭转
  3. 当一个对象必须告诉其它对象,然而却不晓得具体对象到底是谁。换句话说,你不心愿这些对象是严密耦合的。

vue 对于观察者模式的应用

vue 应用到观察者模式的中央有很多,这里咱们次要谈谈对于数据初始化这一块的。

var vm = new Vue({data () {
    return {a: 'hello vue'}
  }
})

1、实现数据劫持

上图咱们能够看到,vue 是利用的是 Object.defineProperty() 对数据进行劫持。并在数据传递变更的时候封装了一层中转站,即咱们看到的 DepWatcher 两个类。

这一大节,咱们只看如何通过观察者模式对数据进行劫持。

1.1、递归遍历

咱们都晓得,vue 对于 data 外面的数据都做了劫持的,那只能对对象进行遍历从而实现每个属性的劫持,源码具体如下

walk (obj: Object) {const keys = Object.keys(obj)
  // 遍历将其变成 vue 的拜访器属性
  for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i], obj[keys[i]])
  }
}
参考 vue 源码视频解说:进入学习

1.2、公布 / 订阅

从下面对象的遍历咱们看到了 defineReactive,那么劫持最要害的点也在于这个函数,该函数外面封装了 gettersetter 函数,应用观察者模式,相互监听

// 设置为拜访器属性,并在其 getter 和 setter 函数中,应用公布 / 订阅模式,相互监听。export function defineReactive (
  obj: Object,
  key: string,
  val: any
) {
  // 这里用到了观察者(公布 / 订阅)模式进行了劫持封装,它定义了一种一对多的关系,让多个观察者监听一个主题对象,这个主题对象的状态产生扭转时会告诉所有观察者对象,观察者对象就能够更新本人的状态。// 实例化一个主题对象,对象中有空的观察者列表
  const dep = new Dep()

  // 获取属性描述符对象(更多的为了 computed 外面的自定义 get 和 set 进行的设计)
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {return}

  const getter = property && property.get
  const setter = property && property.set

  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 收集依赖,建设一对多的的关系,让多个观察者监听以后主题对象
    get: function reactiveGetter () {const value = getter ? getter.call(obj) : val
      if (Dep.target) {dep.depend()
        if (childOb) {childOb.dep.depend()
          // 这里是对数组进行劫持
          if (Array.isArray(value)) {dependArray(value)
          }
        }
      }
      return value
    },
    // 劫持到数据变更,并公布音讯进行告诉
    set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {return}
      if (setter) {setter.call(obj, newVal)
      } else {val = newVal}
      childOb = observe(newVal)
      dep.notify()}
  })
}

1.3、返回 Observer 实例

下面咱们看到了observe 函数,外围就是返回一个 Observer 实例

return new Observer(value)

2、音讯封装,实现 “ 中转站 ”

首先咱们要了解,为什么要做一层消息传递的封装?

咱们在解说观察者模式的时候有提到它的 适用性。这里也同理,咱们在劫持到数据变更的时候,并进行数据变更告诉的时候,如果不做一个 ” 中转站 ” 的话,咱们基本不晓得到底谁订阅了音讯,具体有多少对象订阅了音讯。

这就好比上文中我提到的故事中的密探 A(发布者)和 B(订阅者)。密探 A 与 B 进行信息传递,两人都晓得对方这么一个人的存在,但密探 A 不晓得具体 B 是谁以及到底有多少(订阅者)订阅着本人,可能很多都订阅着密探 A 的信息,so 密探 A(发布者)须要通过 暗号 收集到所有订阅着其音讯的(订阅者),这里对于订阅者的收集其实就是一层 封装。而后密探 A 只需将音讯公布进来,而订阅者们承受到告诉,只管进行本人的 update 操作即可。

简略一点,即收集完订阅者们的密探 A 只管公布音讯,B 以及更多的只管订阅音讯并进行对应的 update 操作,每个模块确保其独立性,实现 高内聚低耦合 这两大准则。

废话不多说,咱们接下来间接开始讲 vue 是如何做的音讯封装的

2.1、Dep

Dep,全名 Dependency,从名字咱们也能大略看出 Dep 类是用来做依赖收集的,具体怎么收集呢。咱们间接看源码

let uid = 0

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    // 用来给每个订阅者 Watcher 做惟一标识符,避免反复收集
    this.id = uid++
    // 定义 subs 数组,用来做依赖收集(收集所有的订阅者 Watcher)
    this.subs = []}

  // 收集订阅者
  addSub (sub: Watcher) {this.subs.push(sub)
  }

  depend () {if (Dep.target) {Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null

代码很简短,但它做的事件却很重要

  1. 定义 subs 数组,用来收集订阅者 Watcher
  2. 当劫持到数据变更的时候,告诉订阅者 Watcher 进行 update 操作

源码中,还抛出了两个办法用来操作 Dep.target,具体如下

// 定义收集指标栈
const targetStack = []

export function pushTarget (_target: Watcher) {if (Dep.target) targetStack.push(Dep.target)
  // 扭转指标指向
  Dep.target = _target
}

export function popTarget () {
  // 删除以后指标,重算指向
  Dep.target = targetStack.pop()}

2.2、Watcher

Watcher 意为观察者,它负责做的事件就是订阅 Dep,当Dep 收回消息传递(notify)的时候,所以订阅着 DepWatchers 会进行本人的 update 操作。废话不多说,间接看源码就晓得了。

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    this.cb = cb
    // parse expression for getter
    if (typeof expOrFn === 'function') {this.getter = expOrFn} else {
      // 解析表达式
      this.getter = parsePath(expOrFn)
      if (!this.getter) {this.getter = function () {}}
    }
    this.value = this.get()}

  get () {
    // 将指标收集到指标栈
    pushTarget(this)
    const vm = this.vm

    let value = this.getter.call(vm, vm)
    // 删除指标
    popTarget()

    return value
  }

  // 订阅 Dep,同时让 Dep 晓得本人订阅着它
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // 收集订阅者
        dep.addSub(this)
      }
    }
  }

  // 订阅者 '生产' 动作,当接管到变更时则会执行
  update () {this.run()
  }

  run () {const value = this.get()
    const oldValue = this.value
    this.value = value
    this.cb.call(this.vm, value, oldValue)
  }
}

上述代码中,我删除了一些与目前探讨无关的代码,如果须要进行具体钻研的,能够自行查阅 vue2.5.3 版本的源码。

当初再去看 DepWatcher,咱们须要晓得两个点

  1. Dep 负责收集所有的订阅者 Watcher,具体谁不必管,具体有多少也不必管,只须要通过 target 指向的计算去收集订阅其音讯的 Watcher 即可,而后只须要做好音讯公布 notify 即可。
  2. Watcher 负责订阅 Dep,并在订阅的时候让 Dep 进行收集,接管到 Dep 公布的音讯时,做好其 update 操作即可。

两者看似相互依赖,实则却保障了其独立性,保障了模块的单一性。

更多的利用

vue 还有一些中央用到了 ” 万能 ” 的 观察者模式,比方咱们熟知的组件之间的事件传递,$on 以及 $emit 的设计。

$emit 负责公布音讯,并对订阅者 $on 做对立生产,即执行 cbs 外面所有的事件。

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)
    }
  } else {(vm._events[event] || (vm._events[event] = [])).push(fn)
  }
  return vm
}

Vue.prototype.$emit = function (event: string): Component {
  const vm: Component = this
  let cbs = vm._events[event]
  if (cbs) {cbs = cbs.length > 1 ? toArray(cbs) : cbs
    const args = toArray(arguments, 1)
    for (let i = 0, l = cbs.length; i < l; i++) {cbs[i].apply(vm, args)
    }
  }
  return vm
}

总结

本文探讨了观察者模式的基本概念、实用场景,以及在 vue 源码中的具体利用。这一节将总结一下观察者模式的一些优缺点

  1. 指标和观察者间的形象耦合:一个指标只晓得他有一系列的观察者(指标进行依赖收集),却不晓得其中任意一个观察者属于哪一个具体的类,这样指标与观察者之间的耦合是形象的和最小的。
  2. 反对播送通信:观察者外面的通信,不像其它通常的一些申请须要指定它的接受者。告诉将会主动播送给所有已订阅该指标对象的相干对象,即上文中的 dep.notify()。当然,指标对象并不关怀到底有多少对象对本人感兴趣,它惟一的职责就是告诉它的各位观察者,解决还是疏忽一个告诉取决于观察者自身。
  3. 一些意外的更新:因为一个观察者它本人并不知道其它观察者的存在,它可能对扭转指标的最终代价无所不知。如果观察者间接在指标上做操作的话,可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新,所以个别咱们会把一些操作放在指标外部,防止出现上述的问题。

OK,本文到这就差不多了,更多的源码设计思路细节将在同系列的其它文章中进行一一解读。

观察者模式

首先话题下来,咱们得反诘一下本人,什么是观察者模式?

概念

观察者模式(Observer):通常又被称作为公布 - 订阅者模式。它定义了一种一对多的依赖关系,即当一个对象的状态产生扭转的时候,所有依赖于它的对象都会失去告诉并自动更新,解决了主体对象与观察者之间性能的耦合。

讲个故事

下面对于观察者模式的概念可能会比拟官网化,所以咱们讲个故事来了解它。

  • A:是密探,代号 001(发布者)
  • B:是通信人员,负责与 A 进行机密交接(订阅者)
  • A 日常工作就是在明面采集一些情报
  • B 则负责暗中察看着 A
  • 一旦 A 传递出一些无关音讯(更多时候须要对音讯进行封装传递,前面依据源码具体分析)
  • B 会立马订阅到该音讯,而后做一些绝对应的变更,比如说告诉做一些事件应答的一些动作。

适用性

以下任一场景都能够应用观察者模式

  1. 当一个形象模型有两个方面,其中一个方面依赖于另一方面。讲这两者封装在独立的对象中能够让它们能够各自独立的扭转和复用
  2. 当一个对象的扭转的时候,须要同时扭转其它对象,然而却不晓得具体多少对象有待扭转
  3. 当一个对象必须告诉其它对象,然而却不晓得具体对象到底是谁。换句话说,你不心愿这些对象是严密耦合的。

vue 对于观察者模式的应用

vue 应用到观察者模式的中央有很多,这里咱们次要谈谈对于数据初始化这一块的。

var vm = new Vue({data () {
    return {a: 'hello vue'}
  }
})

1、实现数据劫持

上图咱们能够看到,vue 是利用的是 Object.defineProperty() 对数据进行劫持。并在数据传递变更的时候封装了一层中转站,即咱们看到的 DepWatcher 两个类。

这一大节,咱们只看如何通过观察者模式对数据进行劫持。

1.1、递归遍历

咱们都晓得,vue 对于 data 外面的数据都做了劫持的,那只能对对象进行遍历从而实现每个属性的劫持,源码具体如下

walk (obj: Object) {const keys = Object.keys(obj)
  // 遍历将其变成 vue 的拜访器属性
  for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i], obj[keys[i]])
  }
}
参考 vue 源码视频解说:进入学习

1.2、公布 / 订阅

从下面对象的遍历咱们看到了 defineReactive,那么劫持最要害的点也在于这个函数,该函数外面封装了 gettersetter 函数,应用观察者模式,相互监听

// 设置为拜访器属性,并在其 getter 和 setter 函数中,应用公布 / 订阅模式,相互监听。export function defineReactive (
  obj: Object,
  key: string,
  val: any
) {
  // 这里用到了观察者(公布 / 订阅)模式进行了劫持封装,它定义了一种一对多的关系,让多个观察者监听一个主题对象,这个主题对象的状态产生扭转时会告诉所有观察者对象,观察者对象就能够更新本人的状态。// 实例化一个主题对象,对象中有空的观察者列表
  const dep = new Dep()

  // 获取属性描述符对象(更多的为了 computed 外面的自定义 get 和 set 进行的设计)
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {return}

  const getter = property && property.get
  const setter = property && property.set

  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 收集依赖,建设一对多的的关系,让多个观察者监听以后主题对象
    get: function reactiveGetter () {const value = getter ? getter.call(obj) : val
      if (Dep.target) {dep.depend()
        if (childOb) {childOb.dep.depend()
          // 这里是对数组进行劫持
          if (Array.isArray(value)) {dependArray(value)
          }
        }
      }
      return value
    },
    // 劫持到数据变更,并公布音讯进行告诉
    set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {return}
      if (setter) {setter.call(obj, newVal)
      } else {val = newVal}
      childOb = observe(newVal)
      dep.notify()}
  })
}

1.3、返回 Observer 实例

下面咱们看到了observe 函数,外围就是返回一个 Observer 实例

return new Observer(value)

2、音讯封装,实现 “ 中转站 ”

首先咱们要了解,为什么要做一层消息传递的封装?

咱们在解说观察者模式的时候有提到它的 适用性。这里也同理,咱们在劫持到数据变更的时候,并进行数据变更告诉的时候,如果不做一个 ” 中转站 ” 的话,咱们基本不晓得到底谁订阅了音讯,具体有多少对象订阅了音讯。

这就好比上文中我提到的故事中的密探 A(发布者)和 B(订阅者)。密探 A 与 B 进行信息传递,两人都晓得对方这么一个人的存在,但密探 A 不晓得具体 B 是谁以及到底有多少(订阅者)订阅着本人,可能很多都订阅着密探 A 的信息,so 密探 A(发布者)须要通过 暗号 收集到所有订阅着其音讯的(订阅者),这里对于订阅者的收集其实就是一层 封装。而后密探 A 只需将音讯公布进来,而订阅者们承受到告诉,只管进行本人的 update 操作即可。

简略一点,即收集完订阅者们的密探 A 只管公布音讯,B 以及更多的只管订阅音讯并进行对应的 update 操作,每个模块确保其独立性,实现 高内聚低耦合 这两大准则。

废话不多说,咱们接下来间接开始讲 vue 是如何做的音讯封装的

2.1、Dep

Dep,全名 Dependency,从名字咱们也能大略看出 Dep 类是用来做依赖收集的,具体怎么收集呢。咱们间接看源码

let uid = 0

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    // 用来给每个订阅者 Watcher 做惟一标识符,避免反复收集
    this.id = uid++
    // 定义 subs 数组,用来做依赖收集(收集所有的订阅者 Watcher)
    this.subs = []}

  // 收集订阅者
  addSub (sub: Watcher) {this.subs.push(sub)
  }

  depend () {if (Dep.target) {Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null

代码很简短,但它做的事件却很重要

  1. 定义 subs 数组,用来收集订阅者 Watcher
  2. 当劫持到数据变更的时候,告诉订阅者 Watcher 进行 update 操作

源码中,还抛出了两个办法用来操作 Dep.target,具体如下

// 定义收集指标栈
const targetStack = []

export function pushTarget (_target: Watcher) {if (Dep.target) targetStack.push(Dep.target)
  // 扭转指标指向
  Dep.target = _target
}

export function popTarget () {
  // 删除以后指标,重算指向
  Dep.target = targetStack.pop()}

2.2、Watcher

Watcher 意为观察者,它负责做的事件就是订阅 Dep,当Dep 收回消息传递(notify)的时候,所以订阅着 DepWatchers 会进行本人的 update 操作。废话不多说,间接看源码就晓得了。

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    this.cb = cb
    // parse expression for getter
    if (typeof expOrFn === 'function') {this.getter = expOrFn} else {
      // 解析表达式
      this.getter = parsePath(expOrFn)
      if (!this.getter) {this.getter = function () {}}
    }
    this.value = this.get()}

  get () {
    // 将指标收集到指标栈
    pushTarget(this)
    const vm = this.vm

    let value = this.getter.call(vm, vm)
    // 删除指标
    popTarget()

    return value
  }

  // 订阅 Dep,同时让 Dep 晓得本人订阅着它
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // 收集订阅者
        dep.addSub(this)
      }
    }
  }

  // 订阅者 '生产' 动作,当接管到变更时则会执行
  update () {this.run()
  }

  run () {const value = this.get()
    const oldValue = this.value
    this.value = value
    this.cb.call(this.vm, value, oldValue)
  }
}

上述代码中,我删除了一些与目前探讨无关的代码,如果须要进行具体钻研的,能够自行查阅 vue2.5.3 版本的源码。

当初再去看 DepWatcher,咱们须要晓得两个点

  1. Dep 负责收集所有的订阅者 Watcher,具体谁不必管,具体有多少也不必管,只须要通过 target 指向的计算去收集订阅其音讯的 Watcher 即可,而后只须要做好音讯公布 notify 即可。
  2. Watcher 负责订阅 Dep,并在订阅的时候让 Dep 进行收集,接管到 Dep 公布的音讯时,做好其 update 操作即可。

两者看似相互依赖,实则却保障了其独立性,保障了模块的单一性。

更多的利用

vue 还有一些中央用到了 ” 万能 ” 的 观察者模式,比方咱们熟知的组件之间的事件传递,$on 以及 $emit 的设计。

$emit 负责公布音讯,并对订阅者 $on 做对立生产,即执行 cbs 外面所有的事件。

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)
    }
  } else {(vm._events[event] || (vm._events[event] = [])).push(fn)
  }
  return vm
}

Vue.prototype.$emit = function (event: string): Component {
  const vm: Component = this
  let cbs = vm._events[event]
  if (cbs) {cbs = cbs.length > 1 ? toArray(cbs) : cbs
    const args = toArray(arguments, 1)
    for (let i = 0, l = cbs.length; i < l; i++) {cbs[i].apply(vm, args)
    }
  }
  return vm
}

总结

本文探讨了观察者模式的基本概念、实用场景,以及在 vue 源码中的具体利用。这一节将总结一下观察者模式的一些优缺点

  1. 指标和观察者间的形象耦合:一个指标只晓得他有一系列的观察者(指标进行依赖收集),却不晓得其中任意一个观察者属于哪一个具体的类,这样指标与观察者之间的耦合是形象的和最小的。
  2. 反对播送通信:观察者外面的通信,不像其它通常的一些申请须要指定它的接受者。告诉将会主动播送给所有已订阅该指标对象的相干对象,即上文中的 dep.notify()。当然,指标对象并不关怀到底有多少对象对本人感兴趣,它惟一的职责就是告诉它的各位观察者,解决还是疏忽一个告诉取决于观察者自身。
  3. 一些意外的更新:因为一个观察者它本人并不知道其它观察者的存在,它可能对扭转指标的最终代价无所不知。如果观察者间接在指标上做操作的话,可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新,所以个别咱们会把一些操作放在指标外部,防止出现上述的问题。

OK,本文到这就差不多了,更多的源码设计思路细节将在同系列的其它文章中进行一一解读。

退出移动版