Vue源码系列之依赖收集

76 阅读8分钟

在之前的 Computed 源码讲解的文章中有提到,Computed 属性值的更新其实是通过 Watcher 来实现的。而在 Vue 官方文档的深入响应式原理里面也有提到,每个组件实例都对应一个 watcher 实例,它会在组件渲染过程中把“接触”的数据记录为依赖。然后当依赖项的 setter 触发时,会通知 watcher 使它关联的组件重新渲染。官方给出的示例图如下:

通过官方给出的图可以看到,在紫色那个圆圈里,代表的是用户定义的 Data 对象,getter 和 setter 是这个对象内定义的每个数据的 getter 和 setter 方法。由此可以看到当用户定义的数据被访问时,会通过 getter 对访问者进行依赖收集,当定义的数据被修改时,会通过 setter 通知 watcher 进行视图更新。这里的依赖收集其实就是把依赖者存储到 Dep 里,而通知 watcher 更新视图,也是通过 Dep 里的方法实现的。所以接下来我们就看看 Dep 和 Watcher 是怎么实现的,它们之间又是怎么关联起来的。

先来看一下它们各自的实现:

Dep

// https://github.com/vuejs/vue/blob/v2.6.12/src/core/observer/dep.js
/* @flow */

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

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  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()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
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]
}

从 Dep 源码和源码中的类型注释可以看出,Dep 主要实现了这样几个功能:

  • addSub: 收集 Watcher 并保存到 subs 数组里;
  • removeSub:移除依赖者(Watcher)
  • depend:如果存在依赖者,调用依赖者的 addDep 方法,Dep.target 是当前全局唯一 Watcher
  • notify:通知依赖者进行更新
  • pushTarget:把当前 Watcher 保存到管理 Watcher 的数组中,同时把当前 Watcher 保存为全局唯一 Watcher,以此确保同一时间只能有一个 Watcher 被计算
  • popTarget:从管理 Watcher 的数组中删除最后添加的 Watcher,并将当前全局唯一 Watcher 回退到上一个 Watcher,此时说明当前已经退出了最后添加的 Watcher 计算

通过 Dep 内定义的方法也能够看出来,Dep 其实就是对 Watcher 的一种管理,那我们再看看 Watcher 的实现:

Watcher

// https://github.com/vuejs/vue/blob/v2.6.12/src/core/observer/watcher.js
/* @flow */

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  noop
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        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
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.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
  }

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

  /**
   * Clean up for dependency collection.
   */
  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
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

在 Watcher 源码里我们能看到一些和 Dep 相关以及另外一些和计算取值相关的操作,那么这里如果单独说每一个方法的实现和功能也不太方便理解,我们接下来还是借着上一篇 Computed 源码里面提到的 computed watcher 来串联一下整个依赖收集过程,在过程中能更好的理解它们的方法。

依赖收集过程

在上一篇文章中讲到,在对计算属性初始化时,initComputed 方法内会为每一个计算属性都生成一个 Watcher 实例对象,也就是 computed watcher:

function initComputed (vm: Component, computed: Object) {
  ...
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
	...
  }
}

当一个计算属性被访问时,会首先触发该计算属性的 getter 方法,从上一篇文章中我们能够知道,其实计算属性的 getter 内执行的是 computed watcher 的相关方法,再把 getter 源码贴出来,帮助我们回顾一下:

...
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
...

那么 getter 方法执行的时候,首先是判断了一下 watcher.dirtywatcher.dirty 我们在 Computed 源码解读的文章里有说过,这是实现计算属性基于它们的响应式依赖进行缓存的实现关键。即通过 dirty 的值判断计算属性的值是否发生了改变,有兴趣的可以去看一下上一篇文章,这里不再细讲。我们这里把这次的计算属性访问当作第一次被访问,这里 dirty 的值是 true,那么下边就会执行watcher.evaluate 方法,进行求值。

evaluate 方法内,做了两个操作,一个是通过 get 方法取值并将值保存到 this.value 上,另外一个是修改 this.dirty 的值为 false,以此进行值的缓存。在这个过程中有一个重要的过程就是 this.getter.call 的执行,getter 就是用户定义计算属性时的方法,当执行 getter 时,就会访问到计算属性依赖的数据对象,执行数据对象的 getter 方法。我们再来看一下数据对象的 getter 方法定义:

// https://github.com/vuejs/vue/blob/v2.6.12/src/core/observer/index.js#L135
...
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

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

defineReactive 是将数据对象转成响应式数据对象的一个方法,该方法定义了数据对象的 getter,同时也为数据对象创建了一个 Dep 实例对象,当数据对象被访问时,在数据对象的 getter 内会执行 dep.depend,结合 Dep 的源码,这时会执行 Dep.target.addDep(this),而在 computed watcher 的 get 方法执行时,就通过 Dep 的 pushTarget 方法把当前的 computed watcher 赋值给了 Dep.target,也就是说这里执行 Dep.target.addDep 方法其实就是在执行 computed watcher 的 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(this) 方法,也就是把当前的 computed watcher 保存到了这个数据对象的 dep 实例对象内。到这里已经完成了一次依赖收集,计算属性依赖的每个数据对象都会执行自己的 getter,从而每一个被依赖的数据对象内的 dep 实例对象都收集到了这个 computed watcher。这还不算完,我们再继续往下看 get 方法。

get 方法在执行完 this.getter.call() 拿到数据后,会继续执行 popTarget() 方法,即推出当前的 computed watcher,恢复到上一个 wacher ,那么上一个 watcher 是谁呢,我们以初次渲染为例,这里的 watcher 其实就是 render watcher,render watcher 是在 Vue 的 mount 阶段通过 Watcher 实例化生成的,具体的我们暂时可以不管,知道就可。那么我们以初次渲染为例,这里的 Dep.target 其实已经变成了 render watcher。到这里,get 也执行完了,我们在回到计算属性的 getter 里。

计算属性的 getter 里执行完 evaluate 计算取值的方法后,接着会判断当前是否有 Watcher 存在,如果存在,执行 watcher.depend

depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

watcher.depend 内把 watcher 收集的 dep 都遍历一遍,同时执行了每一个 dep 的 depend 方法,而 dep 的 depend 方法内执行的是 Dep.target.addDep(this),按我们上边说的,这里的 Dep.target 其实已经是 render watcher 了,所以按照上边的依赖收集过程,render watcher 收集了计算属性收集到的 dep,然后计算属性依赖的数据对象也就订阅了 render watcher,又完成了一次依赖收集。而这一步的依赖收集也就是实现计算属性更新视图的关键了。不得不说 Vue 的设计真的是很巧妙的。

从这个过程中我们看到了两次的依赖收集,先是计算属性收集依赖的数据对象的 dep,数据对象订阅计算属性的 watcher,然后是渲染 watcher 收集计算属性依赖的数据对象的 dep,数据对象也订阅了渲染 watcher。对于其中的 render watcher 和数据对象的 getter 我会在后边的文章中陆续的解释给大家。

最近是刚开始写文章,不写不知道,写了才发现能写好文章的大牛们是真的牛哇,太难了😭!但是既然做了,我一定会坚持下去,慢慢打磨,找到写好文章的方法,加油~~如果大家有什么指导意见,可以在下边留言,军功章上将有你的一半😊~