[Vue源码学习] 响应式原理(中)

221 阅读4分钟

系列文章

前言

从上一章节中,我们知道,在初始化Vue实例的过程中,会将data选项中的普通数据转换成响应式数据,但是如果不进行访问或设置,是无法起到效果的,那么接下来,就来看看Vue是如何进行依赖收集的。

观察者模式与依赖收集

在看具体代码之前,我们需要了解在Vue中是如何运用观察者模式和依赖收集的。

在观察者模式中,目标对象会管理一个集合,里面保存着所有依赖于该目标对象的观察者,当目标对象发生变化时,会通知集合中所有的观察者进行更新。在Vue中,目标对象就是每一个经过defineReactive方法处理过的数据(也包含Observer对象本身),观察者就是一个个Watcher,它可以是渲染Watcher、计算Watcher、自定义Watcher等,而在调用defineReactive方法处理数据的过程中,会为每个数据创建一个Dep的实例,这个实例就是用来管理所有观察该目标对象的观察者集合,当数据发生变化时,Vue就会调用dep.notify方法,从而通知Dep中所有的观察者进行更新。

通过观察者模式,我们可以知道每个目标对象对应的观察者,而依赖收集解决的是每个观察者依赖了哪些数据,之所以这么设计,是为了在更新的过程中,通过重新计算观察者所依赖的目标数据,从而取消对多余的目标数据的观察,同时在目标数据的集合中移除该观察者,避免当目标数据再次发生变化时,产生额外的更新操作。那么接下来,就从源码的角度来看看Vue是如何实现该过程的。

从前面的$mount章节中,我们知道,每个组件实例都会创建一个渲染Watcher,在创建渲染Watcher的最后,会调用get方法,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    this.value = this.lazy
      ? undefined
      : 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
  }
}
/* core/observer/dep.js */
Dep.target = null
const targetStack = []

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

export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

可以看到,在get方法中,首先会调用pushTarget方法,将当前Watcher赋值给Dep.target,然后调用getter方法,也就是在mountComponent方法中传入的updateComponent方法,在其中会调用组件的render渲染函数,而在创建VNode的过程中,如果需要访问data选项中的数据,那么就会触发数据的get访问器:

/* core/observer/index.js */
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  // ...
  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) {
      // ...
    }
  })
}

可以看到,当触发get访问器时,这里的Dep.target指向的就是渲染Watcher,所以此时会调用此数据关联的depdepend方法,进行依赖收集,代码如下所示:

/* core/observer/dep.js */
export default class Dep {
  addSub(sub: Watcher) {
    this.subs.push(sub)
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}
/* core/observer/watcher.js */
export default class Watcher {
  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)
      }
    }
  }
}

可以看到,depend方法并没有简单的调用addSub方法,将观察者添加到目标对象的集合中,而是继续调用watcher.addDep方法,在此方法中,首先将目标对象对应dep添加到watcher.newDeps中,这一步就是之前所说的依赖收集,如果检测到该dep不存在于上一次该观察者所依赖的watcher.depIds中,说明这是一份新的依赖数据,才会调用dep.addSub方法,将该watcher实例添加到目标对象的dep集合中,完成观察者模式中的观察操作。

所以在调用dep.depend方法的过程中,经过上面的步骤后,目标对象的dep中包含着Watcher实例,同时Watcher实例中同样也包含着目标对象的dep。在get访问器中,还有另一段逻辑,当childOb存在时,会继续调用childOb.dep.depend方法,从上一小节中可以知道,假如调用defineReactive方法,且对应的数据还是对象时,会继续调用observe方法,将嵌套对象转换为响应式对象,所以这里的childOb就是嵌套对象的Observer实例,这里的childOb.dep就是Observer实例对应的dep,在调用depend方法后,相当于将嵌套对象也添加到当前Watcher中,建立两者之间的联系,所以直接修改嵌套对象时(this.obj.nested = {}),同样可以触发更新操作。

render渲染函数执行完成后,当前组件的渲染Watcher就可以知道,它依赖了哪些数据,而这些数据中也保存有该渲染Watcher的引用。在调用完getter方法后,会调用popTarget方法,恢复Dep.target的引用,最后还会调用cleanupDeps方法,这个方法就是用来取消对多余的目标对象的观察,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  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方法之前,需要了解Watcher对象中的depsnewDepsdepIdsnewDepIds四个属性,在Watcher每次进行重新渲染之前,此时的newDepsnewDepIds是空的,而depsdepIds保存的是上一次渲染所依赖的数据,在渲染的过程中,newDepsnewDepIds会收集本次渲染所依赖的数据,同时将新增的数据添加到depsdepIds中,在渲染完成后,就会调用cleanupDeps方法,可以看到,此时depsdepIds中的数据可能会比newDepsnewDepIds中的数据多,这部分多出来的数据,其实就是该Watcher不需要依赖的数据,需要进行移除。

cleanupDeps方法中,首先遍历deps对象中的所有数据,如果检测在newDepIds中不存在,那么就调用dep.removeSub(this),从目标数据中的dep中移除当前Watcher,这样一来,当该数据再次发生变化时,就不会通知该Watcher了,然后交换depsnewDepsdepIdsnewDepIds对象,然后将newDepsnewDepIds置空,这样就满足了在下次重新渲染时,newDepsnewDepIds是空的,depsdepIds保存的是上一次渲染所依赖的数据。

总结

Vue通过观察者模式和依赖收集,实现了在每次重新渲染的过程中,重新收集本次渲染所依赖的数据,这样一来,当依赖的数据发生变化时,就可以准确的通知相关的Watcher进行更新操作。