3、vue依赖收集

image

Vue 会把普通对象变成响应式对象,响应式对象 getter 相关的逻辑就是作依赖收集,这一节咱们来详细分析这个过程vue

Dep

Dep 是整个 getter 依赖收集的核心,它的定义在 src/core/observer/dep.js 中node

import type Watcher from './watcher'
import { remove } from '../util/index'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
// 一个静态属性 target,这是一个全局惟一 Watcher
// 同一时间只能有一个全局的 Watcher 被计算
  static target: ?Watcher;
  id: number;
  // 自身属性 subs 也是 Watcher 的数组
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, 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
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
使用 Object.defineProperty 函数定义访问器属性
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    // 省略...
  },
  set: function reactiveSetter (newVal) {
    // 省略...
})

当执行完以上代码实际上 defineReactive 函数就执行完毕了,对于访问器属性的 get 和 set 函数是不会执行的,由于此时没有触发属性的读取和设置操做。react

当属性被读取的时候都作了哪些事情,get 函数

get 函数作了两件事:正确地返回属性值以及收集依赖算法

get: function reactiveGetter () {
// 正确地返回属性值,有getter就使用,没有放val
  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
}

首先判断 Dep.target 是否存在,那么 Dep.target 是什么呢?其实 Dep.target 与咱们在 数据响应系统基本思路 一节中所讲的 Target 做用相同,因此 Dep.target 中保存的值就是要被收集的依赖(观察者)。因此若是 Dep.target 存在的话说明有依赖须要被收集,这个时候才须要执行 if 语句块内的代码,若是 Dep.target 不存在就意味着没有须要被收集的依赖,因此固然就不须要执行 if 语句块内的代码了。express

在 if 语句块内第一句执行的代码就是:dep.depend(),执行 dep 对象的 depend 方法将依赖收集到 dep 中,这里的 dep 对象就是属性的 getter/setter 经过闭包引用的“筐”。数组

接着又判断了 childOb 是否存在,若是存在那么就执行 childOb.dep.depend(),这段代码是什么意思呢?要想搞清楚这段代码的做用,你须要知道 childOb 是什么,前面咱们分析过,假设有以下数据对象:闭包

const data = {
  a: {
    b: 1
  }
}
该数据对象通过观测处理以后,将被添加 __ob__ 属性,以下:

const data = {
  a: {
    b: 1,
    __ob__: {value, dep, vmCount}
  },
  __ob__: {value, dep, vmCount}
}

对于属性 a 来说,访问器属性 a 的 setter/getter 经过闭包引用了一个 Dep 实例对象,即属性 a 用来收集依赖的“筐”。除此以外访问器属性 a 的 setter/getter 还经过闭包引用着 childOb,且 childOb === data.a.__ob__ 因此 childOb.dep === data.a.__ob__.dep。也就是说 childOb.dep.depend() 这句话的执行说明除了要将依赖收集到属性 a 本身的“筐”里以外,还要将一样的依赖收集到 data.a.__ob__.dep 这里”筐“里,为何要将一样的依赖分别收集到这两个不一样的”筐“里呢?其实答案就在于这两个”筐“里收集的依赖的触发时机是不一样的,即做用不一样,两个”筐“以下:异步

  • 第一个”筐“是 dep
  • 第二个”筐“是 childOb.dep

第一个”筐“里收集的依赖的触发时机是当属性值被修改时触发,即在 set 函数中触发:dep.notify()。而第二个”筐“里收集的依赖的触发时机是在使用 $set 或 Vue.set 给数据对象添加新属性时触发,咱们知道因为 js 语言的限制,在没有 Proxy 以前 Vue 没办法拦截到给对象添加属性的操做。因此 Vue 才提供了 $set 和 Vue.set 等方法让咱们有能力给对象添加新属性的同时触发依赖,那么触发依赖是怎么作到的呢?就是经过数据对象的 ob 属性作到的。由于 ob.dep 这个”筐“里收集了与 dep 这个”筐“一样的依赖。假设 Vue.set 函数代码以下:函数

Vue.set = function (obj, key, val) {
  defineReactive(obj, key, val)
  obj.__ob__.dep.notify()
}

当咱们使用上面的代码给 data.a 对象添加新的属性:工具

Vue.set(data.a, 'c', 1)

上面的代码之因此可以触发依赖,就是由于 Vue.set 函数中触发了收集在 data.a.__ob__.dep 这个”筐“中的依赖:

Vue.set = function (obj, key, val) {
  defineReactive(obj, key, val)
  obj.__ob__.dep.notify() // 至关于 data.a.__ob__.dep.notify()
}

Vue.set(data.a, 'c', 1)

因此 ob 属性以及 ob.dep 的主要做用是为了添加、删除属性时有能力触发依赖,而这就是 Vue.set 或 Vue.delete 的原理

在 childOb.dep.depend() 这句话的下面还有一个 if 条件语句,以下:

if (Array.isArray(value)) {
  dependArray(value)
}

若是读取的属性值是数组,那么须要调用 dependArray 函数逐个触发数组每一个元素的依赖收集,为何这么作呢?那是由于 Observer 类在定义响应式属性时对于纯对象和数组的处理方式是不一样,对于上面这段 if 语句的目的等到咱们讲解到对于数组的处理时,会详细说明。

渲染函数的观察者 render watcher

不管是完整版 Vue 的 $mount 函数仍是运行时版 Vue 的 $mount 函数,他们最终都将经过 mountComponent 函数去真正的挂载组件,接下来咱们就看一看在 mountComponent 函数中发生了什么,打开 src/core/instance/lifecycle.js 文件找到 mountComponent 以下:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 省略...
}

继续查看 mountComponent 函数的代码,接下来是一段 if 语句块:

if (!vm.$options.render) {
  vm.$options.render = createEmptyVNode
  if (process.env.NODE_ENV !== 'production') {
    /* istanbul ignore if */
    if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
      vm.$options.el || el) {
      warn(
        'You are using the runtime-only build of Vue where the template ' +
        'compiler is not available. Either pre-compile the templates into ' +
        'render functions, or use the compiler-included build.',
        vm
      )
    } else {
      warn(
        'Failed to mount component: template or render function not defined.',
        vm
      )
    }
  }
}

这段 if 条件语句块首先检查渲染函数是否存在,即 vm.$options.render 是否为真,若是不为真说明渲染函数不存在,这时将会执行 if 语句块内的代码,在 if 语句块内首先将 vm.$options.render 的值设置为 createEmptyVNode 函数,也就是说此时渲染函数的做用将仅仅渲染一个空的 vnode 对象,而后在非生产环境下会根据相应的状况打印警告信息。

在上面这段 if 语句块的下面,执行了 callHook 函数,触发 beforeMount 生命周期钩子:

callHook(vm, 'beforeMount')

在触发 beforeMount 生命周期钩子以后,组件将开始挂载工做,首先是以下这段代码:

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  updateComponent = () => {
    const name = vm._name
    const id = vm._uid
    const startTag = `vue-perf-start:${id}`
    const endTag = `vue-perf-end:${id}`

    mark(startTag)
    const vnode = vm._render()
    mark(endTag)
    measure(`vue ${name} render`, startTag, endTag)

    mark(startTag)
    vm._update(vnode, hydrating)
    mark(endTag)
    measure(`vue ${name} patch`, startTag, endTag)
  }
} else {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
}

这段代码的做用只有一个,即定义并初始化 updateComponent 函数,这个函数将用做建立 Watcher 实例时传递给 Watcher 构造函数的第二个参数,这也将是咱们第一次真正地接触 Watcher 构造函数,不过如今咱们须要先把 updateComponent 函数搞清楚,在上面的代码中首先定义了 updateComponent 变量,虽然是一个 if...else 语句块,其中 if 语句块的条件咱们已经遇到过不少次了,在知足该条件的状况下会作一些性能统计,能够看到在 if 语句块中分别统计了 vm._render() 函数以及 vm._update() 函数的运行性能。也就是说不管是执行 if 语句块仍是执行 else 语句块,最终 updateComponent 函数的功能是不变的。

既然功能相同,咱们就直接看 else 语句块的代码,由于它要简洁的多

let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  // 省略...
} else {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
}

能够看到 updateComponent 是一个函数,该函数的做用是以 vm._render() 函数的返回值做为第一个参数调用 vm._update() 函数。因为咱们尚未讲解 vm._render 函数和 vm._update 函数的做用,因此为了让你们更好理解,咱们能够简单地认为:

  • vm._render 函数的做用是调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)
  • vm._update 函数的做用是把 vm._render 函数生成的虚拟节点渲染成真正的 DOM

也就是说目前咱们能够简单地认为 updateComponent 函数的做用就是:把渲染函数生成的虚拟DOM渲染成真正的DOM,其实在 vm._update 内部是经过虚拟DOM的补丁算法(patch)来完成的

再往下,咱们将遇到建立观察者(Watcher)实例的代码:

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

由于 watcher 对表达式的求值,触发了数据属性的 get 拦截器函数,从而收集到了依赖,当数据变化时可以触发响应。在上面的代码中 Watcher 观察者实例将对 updateComponent 函数求值,咱们知道 updateComponent 函数的执行会间接触发渲染函数(vm.$options.render)的执行,而渲染函数的执行则会触发数据属性的 get 拦截器函数,从而将依赖(观察者)收集,当数据变化时将从新执行 updateComponent 函数,这就完成了从新渲染。同时咱们把上面代码中实例化的观察者对象称为 渲染函数的观察者

初识 Watcher

接下来咱们就以渲染函数的观察者对象为例,顺着脉络了解 Watcher 类,Watcher 类定义在 src/core/observer/watcher.js 文件中,以下是 Watcher 类的所有内容:

export default class Watcher {
    // 实例时能够传递五个参数
  constructor (
    // 组件实例对象 vm
    vm: Component,
    // 要观察的表达式 expOrFn
    expOrFn: string | Function,
    // 当被观察的表达式的值变化时的回调函数 cb
    cb: Function,
    // 一些传递给当前观察者对象的选项 options 
    options?: ?Object,
    // 一个布尔值 isRenderWatcher 用来标识该观察者实例是不是渲染函数的观察者。
    isRenderWatcher?: boolean
  ) {

  }

  get () {
    // 省略...
  }

  addDep (dep: Dep) {
    // 省略...
  }

  cleanupDeps () {
    // 省略...
  }

  update () {
    // 省略...
  }

  run () {
    // 省略...
  }

  getAndInvoke (cb: Function) {
    // 省略...
  }

  evaluate () {
    // 省略...
  }

  depend () {
    // 省略...
  }

  teardown () {
    // 省略...
  }
}

以下是在 mountComponent 函数中建立渲染函数观察者实例的代码:

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

在建立渲染函数观察者实例对象时传递了所有的五个参数

  • 第一个参数 vm 很显然就是当前组件实例对象;
  • 第二个参数 updateComponent 就是被观察的目标,它是一个函数;
  • 第三个参数 noop 是一个空函数;
  • 第四个参数是一个包含 before 函数的对象,这个对象将做为传递给该观察者的选项;
  • 第五个参数为 true,咱们知道这个参数标识着该观察者实例对象是不是渲染函数的观察者,很显然上面的代码是在为渲染函数建立观察者对象,因此第五个参数天然为 true

这里有几个问题须要注意,首先被观察的表达式是一个函数,即 updateComponent 函数,咱们知道 Watcher 的原理是经过对“被观测目标”的求值,触发数据属性的 get 拦截器函数从而收集依赖,至于“被观测目标”究竟是表达式仍是函数或者是其余形式的内容都不重要,重要的是“被观测目标”可否触发数据属性的 get 拦截器函数,很显然函数是具有这个能力的。另一个咱们须要注意的是传递给 Watcher 构造函数的第三个参数 noop 是一个空函数,它什么事情都不会作,有的同窗可能会有疑问:“不是说好了当数据变化时从新渲染吗,如今怎么什么都不作了?”,实际上数据的变化不只仅会执行回调,还会从新对“被观察目标”求值,也就是说 updateComponent 也会被调用,因此不须要经过执行回调去从新渲染。说到这里你们或许又产生了一个疑问:“再次执行 updateComponent 函数难道不会致使再次触发数据属性的 get 拦截器函数致使重复收集依赖吗?”,这是个好问题,不过不用担忧,由于 Vue 已经实现了避免收集重复依赖的处理,咱们后面会讲到的。

constructor 函数开始,看一下建立渲染函数观察者实例对象的过程,进一步了解一个观察者,以下是 constructor 函数开头的一段代码:

this.vm = vm
if (isRenderWatcher) {
  vm._watcher = this
}
vm._watchers.push(this)

首先将当前组件实例对象 vm 赋值给该观察者实例的 this.vm 属性,也就是说每个观察者实例对象都有一个 vm 实例属性,该属性指明了这个观察者是属于哪个组件的。接着使用 if 条件语句判断 isRenderWatcher 是否为真,前面说过 isRenderWatcher 标识着是不是渲染函数的观察者,只有在 mountComponent 函数中建立渲染函数观察者时这个参数为真,若是 isRenderWatcher 为真那么则会将当前观察者实例赋值给 vm._watcher 属性,也就是说组件实例的 _watcher 属性的值引用着该组件的渲染函数观察者。你们还记得 _watcher 属性是在哪里初始化的吗?是在 initLifecycle 函数中被初始化的,其初始值为 null。在 if 语句块的后面将当前观察者实例对象 push 到 vm._watchers 数组中,也就是说属于该组件实例的观察者都会被添加到该组件实例对象的 vm._watchers 数组中,包括渲染函数的观察者和非渲染函数的观察者。另外组件实例的 vm._watchers 属性是在 initState 函数中初始化的,其初始值是一个空数组。

// 判断是否传递了 options 参数
if (options) {
  this.deep = !!options.deep
  this.user = !!options.user
  this.computed = !!options.computed
  this.sync = !!options.sync
  this.before = options.before
} else {
  this.deep = this.user = this.computed = this.sync = false
}
  • options.deep,用来告诉当前观察者实例对象是不是深度观测
    咱们平时在使用 Vue 的 watch 选项或者 vm.$watch 函数去观测某个数据时,能够经过设置 deep 选项的值为 true 来深度观测该数据。

  • options.user,用来标识当前观察者实例对象是 开发者定义的 仍是 内部定义的
    实际上不管是 Vue 的 watch 选项仍是 vm.$watch 函数,他们的实现都是经过实例化 Watcher 类完成的,等到咱们讲解 Vue 的 watch 选项和 vm.$watch 的具体实现时你们会看到,除了内部定义的观察者(如:渲染函数的观察者、计算属性的观察者等)以外,全部观察者都被认为是开发者定义的,这时 options.user 会自动被设置为 true。

  • options.computed,用来标识当前观察者实例对象是不是计算属性的观察者
    这里须要明确的是,计算属性的观察者并非指一个观察某个计算属性变化的观察者,而是指 Vue 内部在实现计算属性这个功能时为计算属性建立的观察者。等到咱们讲解计算属性的实现时再详细说明。

  • options.sync,用来告诉观察者当数据变化时是否同步求值并执行回调
    默认状况下当数据变化时不会同步求值并执行回调,而是将须要从新求值并执行回调的观察者放到一个异步队列中,当全部数据的变化结束以后统一求值并执行回调,这么作的好处有不少,咱们后面会详细讲解。

  • options.before,能够理解为 Watcher 实例的钩子,当数据变化以后,触发更新以前,调用在建立渲染函数的观察者实例对象时传递的 before 选项。

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

能够看到当数据变化以后,触发更新以前,若是 vm._isMounted 属性的值为真,则会调用 beforeUpdate 生命周期钩子。

再往下又定义了一些实例属性,以下:

// 定义了 this.cb 属性,它的值为 cb 回调函数
this.cb = cb
// 定义了 this.id 属性,它是观察者实例对象的惟一标识。
this.id = ++uid // uid for batching
// 定义了 this.active 属性,它标识着该观察者实例对象是不是激活状态,默认值为 true 表明激活。
this.active = true
//定义了 this.dirty 属性,该属性的值与 this.computed 属性的值相同,也就是说只有计算属性的观察者实例对象的 this.dirty 属性的值才会为真,由于计算属性是惰性求值。
this.dirty = this.computed // for computed watchers

接着往下看代码,以下:

// 其实它们就是传说中用来实现避免收集重复依赖,且移除无用依赖的功能也依赖于它们

this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()

来到一段 if...else 语句块:

if (typeof expOrFn === 'function') {
  this.getter = expOrFn
} else {
  this.getter = parsePath(expOrFn)
  if (!this.getter) {
    this.getter = function () {}
    process.env.NODE_ENV !== 'production' && warn(
      `Failed watching path: "${expOrFn}" ` +
      'Watcher only accepts simple dot-delimited paths. ' +
      'For full control, use a function instead.',
      vm
    )
  }
}

这段代码检测了 expOrFn 的类型,若是 expOrFn 是函数,那么直接使用 expOrFn 做为 this.getter 属性的值。若是 expOrFn 不是函数,那么将 expOrFn 透传给 parsePath 函数,并以 parsePath 函数的返回值做为 this.getter 属性的值。那么 parsePath 函数作了什么呢?parsePath 函数定义在 src/core/util/lang.js 文件,源码以下:

const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

首先定义 segments 常量,它的值是经过字符 . 分割 path 字符串产生的数组,随后 parsePath 函数将返回值一个函数,该函数的做用是遍历 segments 数组循环访问 path 指定的属性值。这样就触发了数据属性的 get 拦截器函数。但要注意 parsePath 返回的新函数将做为 this.getter 的值,只有当 this.getter 被调用的时候,这个函数才会执行,目的就是支持a.b.c那种多层的对象的调用。

再往下咱们来到了 constructor 函数的最后一段代码:

if (this.computed) {
  this.value = undefined
  this.dep = new Dep()
} else {
  this.value = this.get()
}

经过这段代码咱们能够发现,计算属性的观察者和其余观察者实例对象的处理方式是不一样的,对于计算属性的观察者咱们会在讲解计算属性时详细说明。除计算属性的观察者以外的全部观察者实例对象都将执行如上代码的 else 分支语句,即调用 this.get() 方法。

依赖收集的过程

this.get() 是咱们遇到的第一个观察者对象的实例方法,它的做用能够用两个字描述:求值。求值的目的有两个

  • 第一个是可以触发访问器属性的 get 拦截器函数
  • 第二个是可以得到被观察目标的值

并且可以触发访问器属性的 get 拦截器函数是依赖被收集的关键,下面咱们具体查看一下 this.get() 方法的内容:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

一上来调用了 pushTarget(this) 函数,并将当前观察者实例对象做为参数传递,这里的 pushTarget 函数来自于 src/core/observer/dep.js 文件,以下代码所示:

export default class Dep {
  // 省略...
}

Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

当时咱们说每一个响应式数据的属性都经过闭包引用着一个用来收集属于自身依赖的“筐”,实际上那个“筐”就是 Dep 类的实例对象。更多关于 Dep 类的内容咱们会在合适的地方讲解,如今咱们的主要目的是搞清楚 pushTarget 函数是作什么的。在上面这段代码中咱们能够看到 Dep 类拥有一个静态属性,即 Dep.target 属性,该属性的初始值为 null,其实 pushTarget 函数的做用就是用来为 Dep.target 属性赋值的,pushTarget 函数会将接收到的参数赋值给 Dep.target 属性,咱们知道传递给 pushTarget 函数的参数就是调用该函数的观察者对象,因此 Dep.target 保存着一个观察者对象,其实这个观察者对象就是即将要收集的目标。

this.get() 方法中,以下是简化后的代码:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 省略...
  } finally {
    // 省略...
  }
  return value
}

在调用 pushTarget 函数以后,定义了 value 变量,该变量的值为 this.getter 函数的返回值,咱们知道观察者对象的 this.getter 属性是一个函数,这个函数的执行就意味着对被观察目标的求值,并将获得的值赋值给 value 变量,并且咱们能够看到 this.get 方法的最后将 value 返回为何要强调这一点呢?以下代码所示:

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  // 省略...
  if (this.computed) {
    this.value = undefined
    this.dep = new Dep()
  } else {
    this.value = this.get()
  }
}

这句高亮的代码将 this.get() 方法的返回值赋值给了观察者实例对象的 this.value 属性。也就是说 this.value 属性保存着被观察目标的值。

this.get() 方法除了对被观察目标求值以外,你们别忘了正是由于对被观察目标的求值才得以触发数据属性的 get 拦截器函数,仍是以渲染函数的观察者为例,假设咱们有以下模板:

<div id="demo">
  <p>{{name}}</p>
</div>

这段模板被编译将生成以下渲染函数:

// 编译生成的渲染函数是一个匿名函数
function anonymous () {
  with (this) {
    return _c('div',
      { attrs:{ "id": "demo" } },
      [_v("\n      "+_s(name)+"\n    ")]
    )
  }
}

能够发现渲染函数的执行会读取数据属性 name 的值,这将会触发 name 属性的 get 拦截器函数

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
}

这段代码咱们已经很熟悉了,它是数据属性的 get 拦截器函数,因为渲染函数读取了 name 属性的值,因此 name 属性的 get 拦截器函数将被执行,你们注意如上代码中高亮的两句代码,首先判断了 Dep.target 是否存在,若是存在则调用 dep.depend 方法收集依赖。那么 Dep.target 是否存在呢?答案是存在,这就是为何 pushTarget 函数要在调用 this.getter 函数以前被调用的缘由。既然 dep.depend 方法被执行,那么咱们就找到 dep.depend 方法,以下:

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

为了搞清楚这么作的目的,咱们找到观察者实例对象的 addDep 方法,以下:

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)
    }
  }
}

能够看到 addDep 方法接收一个参数,这个参数是一个 Dep 对象,在 addDep 方法内部首先定义了常量 id,它的值是 Dep 实例对象的惟一 id 值。接着是一段 if 语句块,该 if 语句块的代码很关键,由于它的做用就是用来 避免收集重复依赖 的,既然是用来避免收集重复的依赖,那么就不得不用到咱们前面提到过的两组属性,即 newDepIds、newDeps 以及 depIds、deps。

咱们思考一下可不能够把 addDep 方法修改为以下这样:

addDep (dep: Dep) {
  dep.addSub(this)
}

首先解释一下 dep.addSub 方法,它的源码以下:

addSub (sub: Watcher) {
  this.subs.push(sub)
}

addSub 方法接收观察者对象做为参数,并将接收到的观察者添加到 Dep 实例对象的 subs 数组中,其实 addSub 方法才是真正用来收集观察者的方法,而且收集到的观察者都会被添加到 subs 数组中存起来。

了解了 addSub 方法以后,咱们再回到以下这段代码:

addDep (dep: Dep) {
  dep.addSub(this)
}

咱们修改了 addDep 方法,直接在 addDep 方法内调用 dep.addSub 方法,并将当前观察者对象做为参数传递。这不是很好吗?难道有什么问题吗?固然有问题,假如咱们有以下模板:

<div id="demo">
  {{name}}{{name}}
</div>

这段模板的不一样之处在于咱们使用了两次 name 数据,那么相应的渲染函数也将变为以下这样:

function anonymous () {
  with (this) {
    return _c('div',
      { attrs:{ "id": "demo" } },
      [_v("\n      "+_s(name)+_s(name)+"\n    ")]
    )
  }
}

能够看到,渲染函数的执行将读取两次数据对象 name 属性的值,这必然会触发两次 name 属性的 get 拦截器函数,一样的道理,dep.depend 也将被触发两次,最后致使 dep.addSub 方法被执行了两次,且参数如出一辙,这样就产生了同一个观察者被收集屡次的问题。因此咱们不能像如上那样修改 addDep 函数的代码,那么此时我相信你们也应该知道以下高亮代码的含义了:

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)
    }
  }
}

在 addDep 内部并非直接调用 dep.addSub 收集观察者,而是先根据 dep.id 属性检测该 Dep 实例对象是否已经存在于 newDepIds 中,若是存在那么说明已经收集过依赖了,什么都不会作。若是不存在才会继续执行 if 语句块的代码,同时将 dep.id 属性和 Dep 实例对象自己分别添加到 newDepIds 和 newDeps 属性中,这样不管一个数据属性被读取了多少次,对于同一个观察者它只会收集一次。

!this.depIds.has(id) 是什么意思呢?

newDepIds 属性用来避免在 一次求值 的过程当中收集重复的依赖,其实 depIds 属性是用来在 屡次求值 中避免收集重复依赖的

什么是屡次求值,其实所谓屡次求值是指当数据变化时从新求值的过程。你们可能会疑惑,难道从新求值的时候不能用 newDepIds 属性来避免收集重复的依赖吗?不能,缘由在于每一次求值以后 newDepIds 属性都会被清空,也就是说每次从新求值的时候对于观察者实例对象来说 newDepIds 属性始终是全新的。虽然每次求值以后会清空 newDepIds 属性的值,但在清空以前会把 newDepIds 属性的值以及 newDeps 属性的值赋值给 depIds 属性和 deps 属性,这样从新求值的时候 depIds 属性和 deps 属性将会保存着上一次求值中 newDepIds 属性以及 newDeps 属性的值。为了证实这一点,咱们来看一下观察者对象的求值方法,即 get() 方法:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 省略...
  } finally {
    // 省略...
    popTarget()
    this.cleanupDeps()
  }
  return value
}

能够看到在 finally 语句块内调用了观察者对象的 cleanupDeps 方法,这个方法的做用正如咱们前面所说的那样,每次求值完毕后都会使用 depIds 属性和 deps 属性保存 newDepIds 属性和 newDeps 属性的值,而后再清空 newDepIds 属性和 newDeps 属性的值,以下是 cleanupDeps 方法的源码:

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}

在 cleanupDeps 方法内部,首先是一个 while 循环,咱们暂且不关心这个循环的做用,咱们看循环下面的代码,即高亮的部分,这段代码是典型的引用类型变量交换值的过程,最终的结果就是 newDepIds 属性和 newDeps 属性被清空,而且在被清空以前把值分别赋给了 depIds 属性和 deps 属性,这两个属性将会用在下一次求值时避免依赖的重复收集。

如今咱们能够作几点总结:

  • 一、newDepIds 属性用来在一次求值中避免收集重复的观察者
  • 二、每次求值并收集观察者完成以后会清空 newDepIds 和 newDeps 这两个属性的值,而且在被清空以前把值分别赋给了 depIds 属性和 deps 属性
  • 三、depIds 属性用来避免重复求值时收集重复的观察者

经过以上三点内容咱们能够总结出一个结论,即 newDepIds 和 newDeps 这两个属性的值所存储的老是当次求值所收集到的 Dep 实例对象,而 depIds 和 deps 这两个属性的值所存储的老是上一次求值过程当中所收集到的 Dep 实例对象。

除了以上三点以外,其实 deps 属性还可以用来移除废弃的观察者,cleanupDeps 方法中开头的那段 while 循环就是用来实现这个功能的,以下代码所示:

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  // 省略...
}

这段 while 循环就是对 deps 数组进行遍历,也就是对上一次求值所收集到的 Dep 对象进行遍历,而后在循环内部检查上一次求值所收集到的 Dep 实例对象是否存在于当前此次求值所收集到的 Dep 实例对象中,若是不存在则说明该 Dep 实例对象已经和该观察者不存在依赖关系了,这时就会调用 dep.removeSub(this) 方法并以该观察者实例对象做为参数传递,从而将该观察者对象从 Dep 实例对象中移除。

咱们能够找到 Dep 类的 removeSub 实例方法,以下:

removeSub (sub: Watcher) {
  remove(this.subs, sub)
}

它的内容很简单,接收一个要被移除的观察者做为参数,而后使用 remove 工具函数,将该观察者从 this.subs 数组中移除。

相关文章
相关标签/搜索