响应式原理二:依赖收集

323 阅读3分钟

《响应式原理一:data 初始化》一文中,分析了 data 是如何将一个普通对象变成响应式对象,核心的实现是在函数 defineReactive 中采用 ES5 Object.defineProperty 定义了 getset 函数,其作用是依赖收集和派发更新。那么,本文将分析依赖收集的实现原理。

何时触发 get

在开始之前,先来看看 get 函数的触发时机。

在实例化 Vue,即执行 new Vue 时,不管是用户手动挂载 Vue,还是框架内部挂载 Vue,都会调用到函数 $mount,其内部会调用函数 mountComponent。在该函数的内部实现,有这么一段逻辑:

export function mountComponent {
  ......
  
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ......
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  } 
    
  ......
  
  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
}

需要注意的两点:

一是定义函数 updateComponent

二是实例化 Watcher,此时是一个渲染 Watcher

在实例化渲染 Watcher 的过程中,会触发 get 函数,即

/**
  * Evaluate the getter, and re-collect dependencies.
  */
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
}

需要关注的核心代码:this.getter.call(vm, vm),此时会触发 updateComponent 函数被调用。在执行该函数的过程中,会先执行 render 函数;那么此时会访问数据对象,从而触发响应式对象属性 get 函数调用。

依赖收集

了解了其触发时机,那么就可以来看看是如何实现依赖收集的?代码实现如下:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  
  ......
  
  let childOb = !shallow && 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
    }
  })
  
  ......
}

在触发之前,会先实例化 Dep,为什么需要这样做呢?因为 Dep 是整个依赖收集的核心,简单来说,它是一个订阅中心,需要关注它的变化的观察者可以进行订阅;那么当这个订阅中心发生变化时,则会通知观察者做出相应的改变,采用的是观察者模式。

Dep 有一个静态属性:target,它是一个全局唯一 Watcher,因为在同一时间只能有一个全局的 Watcher 被计算。

接着会调用 dep 方法 depend,其内部实现如下:

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

由于 Dep.target 保存的是 Watcher 实例,那么 Dep.target.addDep(this) 其实是指调用 watcher 对象方法 addDepthis 是指 dep 对象,其内部实现如下:

/**
  * Add a dependency to this directive.
  */
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)
    }
  }
}

这里需要注意有 4 个变量:newDepIdsdepIdsnewDepIdsdepIds,它们是在实例化 Watcher 定义的,如下:

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

depsnewDeps 其数据类型都是数组,表示 Watcher 实例持有 Dep 实例的数组。具体来说,deps 表示上一次添加的 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组。

depIdsnewDepIds 其数据结构是 Set,分别代表 depsnewDepsid

这里会做一些逻辑判断,作用是防止同一条数据被添加多次。然后会调用实例 dep 方法 addSub,即把当前 wacher 添加到订阅列表里 subs,作用是当数据变化时,通过遍历 subs 通知每个 sub,其内部实现如下:

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

回到依赖收集函数 get,执行完实例 dep 方法 depend后,还有一段逻辑,即当 childOb 不为空的时候,会执行 if 逻辑,作用是针对嵌套对象。

由于 childObObserver 实例,那么 childOb.dep.depend() 其实是指调用实例 dep 方法 depend,已分析过。

接着,如果 value 是数组的话,则会调用 dependArray,其内部实现如下:

/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

实现逻辑也挺简单的,对 value 进行遍历,如果满足条件的话,最终还是调用 dep 实例方法 depend。除此之外,如果遍历到的元素是数组的话,则递归调用 dependArray,执行相同的逻辑,直到处理完毕为止。

参考链接