Vue2源码学习笔记 - 13.响应式原理—Watcher 类详解

ObserverDep 类之后,我们迎来了这三个类中最复杂的类——Watcher。Watcher 这个词在 Vue 中有很多叫法:观察者、依赖者及订阅者等,我觉得它们的叫法都挺有道理。Watcher 就像一个哨兵,时刻观察着所需的变量,一有变动就通知其他部件,这里我们也称它为观察者。先来一张 UML 图熟悉下

UML

Vue2源码学习笔记 - 13.响应式原理—Watcher 类详解_第1张图片

使用场景

我们先来看看都有谁使用 Watcher 创建了对象。我们在 Vue 的源码里全局搜索 new Watcher,有三处地方创建了 Watcher 对象,分别是:

1、文件 /src/core/instance/lifecycle.js 的 mountComponent 函数中

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

2、文件 /src/core/instance/state.js 的 initComputed 函数中

...
var computedWatcherOptions = { lazy: true };
// create internal watcher for the computed property.
watchers[key] = new Watcher(
  vm,
  getter || noop,
  noop,
  computedWatcherOptions
)
...

3、文件 /src/core/instance/state.js 的 Vue.prototype.$watch 原型方法中

Vue.prototype.$watch = function (expOrFn: string | Function,
  cb: any,options?: Object): Function {
  ...
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  ...
}

它们分别用于渲染函数、计算属性(computed)和侦听属性(watch),这三者都是需要在特定变量更新时作出响应。这在观察者模式中,变量是被观察者,Watcher 就代表观察者。也可以说是发布-订阅模式,变量及关联的 Dep 对象是发布者,Watcher 是订阅者

Watcher

下面我们来看看 Watcher 的源码,它在文件 /src/core/observer/watcher.js 中定义

...
/**
 * 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,               // Vue类/组件 实例
    expOrFn: string | Function,  // 字符表达式或函数
    cb: Function,                // 回调函数,收到更新通知时执行
    options?: ?Object,           // 其他选项
    isRenderWatcher?: boolean    // 是否为渲染 watcher
  ) {
    this.vm = vm
    if (isRenderWatcher) { // 如果是渲染 watcher,对象赋值给 vm._watcher
      vm._watcher = this
    }
    vm._watchers.push(this) // 对象放入 vm 的 _watchers 中
    // 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 // 实例ID
    this.active = true
    // 初始化 dirty = lazy,主要用于计算属性
    this.dirty = this.lazy // for lazy watchers
    this.deps = []     // 当前观察的 dep
    this.newDeps = []  // 新收集的需要观察的 dep
    this.depIds = new Set()     // deps 的ID集合
    this.newDepIds = new Set()  // newDeps 的ID集合
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    // expOrFn 转成 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
        )
      }
    }
    // 如果不是 lazy 的 watcher 则立即执行 get 成员方法
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
   // 调用 getter,并且收集依赖
  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.
   */
   // 添加 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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
   // 清除旧的 dep,新的 dep 赋给 deps
  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.
   */
   // dep派发更新通知时执行
  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) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } 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.
   */
   // 调用本 watcher 拥有的所有 dep 的 depend 成员方法
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
   // watcher 被销毁,清理相关配置
  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 类有点长,我们先来看构造函数,首先它根据传入构造函数的第五个参数来确定该 Watcher 对象是否为渲染 Watcher,每一个组件都有一个渲染 Wathcer,如果是则把它赋值给 vm._watcher 并放入组件的数组属性 vm._watchers 中。

然后初始化选项,user 是否通过侦听属性创建的 Watcher;lazy 是否为懒求值的 Watcher,通常用于计算属性的延迟求值;dirty 是否脏状态,也是用于延迟求值;cb 通常用于侦听属性的回调函数;sync 接收到更新通知时是否同步执行调度方法;depIds 和 newDepIds 分别为现有 Dep ID 和 新收集的 Dep ID;deps 和 newDeps 分别为现有 Dep 和新收集的 Dep 等。

跟着把传参 expOrFn 转换为 getter 函数,如果 expOrFn 是字符串则通过调用 parsePath 函数转换成函数,它主要是按点切分为属性键用来访问对象的属性值。正如上面提到的,三处不同地方实例化对象时传入的 expOrFn 不同导致各自的 getter 各异,但是它们本质都是引用响应式变量,从而触发依赖收集。

最后判断 this.lazy 的值,假则执行 get 成员方法并返回值赋给 this.value,否则直接赋值 undefined。

get 方法

成员方法 get 是这个类中非常重要的方法,它主要是调用 getter 函数,并在其执行过程中触发依赖收集。我们先用伪代码简单描述一下

// 入栈,备份当前 Dep.target,然后设置为本 Watcher 对象
pushTarget(this)

    // 调用 getter 求值,触发响应式变量的 getter 收集依赖
    value = this.getter.call(vm, vm)
    
    // 如果需要,则对 value 的属性递归求值和收集依赖
    traverse(value)
    
// 出栈,恢复 Dep.target 为之前备份的 Watcher 对象
popTarget()

// 新旧依赖过滤,移除不需要的依赖
this.cleanupDeps()

pushTarget 与 popTarget 函数我们在上一节学习 Dep 类的时候有说到,并且还有生动形象地配图。这里它 pushTarget(this) 把自身对象设置到了全局唯一变量 Dep.target,然后调用 this.getter,这个函数简单的理解成引用变量即可,在引用变量的时候会执行在 Object.defineProperty 中配置的变量的 reactiveGetter 函数,并在里面收集依赖。

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

然后如果需要深度收集的话,会递归调用属性值求值并收集依赖,完成这一系列之后执行 popTarget 恢复 Dep.target。再之后就是通过调用 cleanupDeps 过滤清理新旧依赖关系,这个过程在方法 cleanupDeps 中细说。

addDep 方法

这个方法是用于依赖收集中的,如上代码段,响应式变量的 reactiveGetter 方法中,依赖收集是判断 Dep.target 为真,即是 Dep.target 为 Watcher 对象,则调用变量关联的 Dep 对象的 depend 方法。

// Dep 类 成员方法 depend
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

它相当于又是调用 Watcher 对象的 addDep 方法,并把 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)
    // 新的Deps中,且旧的Deps没有,则加入Dep的subs中
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

addDep 检查如果传入的新 Dep 的 id 不在 newDepIds 中则加入并把 Dep 对象加入 newDeps 中。再判断如果 id 不在 depIds 中则调用 Dep 的 addSub 方法,参数为本 Watcher 对象,这个 addSub 只是把 Watcher 对象放入 Dep 的 subs 数组中,这样 Dep 就拥有了订阅了它的所有 Watcher 对象。

Vue2源码学习笔记 - 13.响应式原理—Watcher 类详解_第2张图片

cleanupDeps 方法

在 get 方法中有看到,这个方法是在 popTarget 恢复栈之后被调用,这个时候对于 getter 中新一轮收集的依赖已经通过 addDep 全部暂存于 newDeps 中了,它们的 ID 也存于 newDepIds 中。

/**
 * Clean up for dependency collection.
 */
cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    // Dep中删除旧的订阅
    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 方法首先遍历 this.deps 里面旧的 Dep,如果其 ID 不在新的 newDepIds 中,则调用 Dep.removeSub 从 Dep.subs 订阅数组中删除该 Watcher,这样该 Watcher 就不会再接收到更新通知。后面的8行代码,就是对调 newDeps 和 deps,depIds 和 newDepIds,然后清空 newDeps 和 newDepIds,那么,有没有简单的写法呢?为什么又要写这么复杂呢?

this.depIds = this.newDepIds
this.newDepIds = new Set()

this.deps = this.newDeps
this.newDeps = []

这4行代码与上面最后8行代码是等效的,比之却简单了很多,但是 Vue 的写法很大程度降低了频繁地创建对象,不管对调多少次,始终都是用的在构造函数中创建的 Set 对象和数组,这是 Vue 代码的一个很好的细节。

update 方法

这个方法很好理解,它是供 Dep 在派发更新通知时主动调用的,它根据不同选项做不同处理,如果 lazy 和 sync 都为假,则通过调用 queueWatcher 把自身放入调度队列,在下一个周期执行调度任务 run 方法。简单来说就是计算属性 watcher 是把 dirty 设为 true,不执行回调;渲染 watcher 和 侦听属性 watcher 则是把 watcher 对象放入队列进行调度。

/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update () {
    // 如果 lazy 为真(延迟求值)
  if (this.lazy) {
    // 设置 dirty 为真,后续在 evaluate 中计算求值
    this.dirty = true
  } else if (this.sync) {
    // 如果 sync 为真,同步执行调度任务 run 方法
    this.run()
  } else {
    // 把本对象加入调度队列
    queueWatcher(this)
  }
}
run 方法

可以称为调度任务,在 update 执行 queueWatcher 放入队列后,在适当的时机会执行该方法,这个细节我们后续会在其他章节说。这个方法会执行方法 get,对如渲染 Watcher 则重新渲染页面并收集依赖,然后比对返回值并判断其他条件觉得是否执行回调函数 this.cb,对于满足条件的侦听属性的回调函数也是在这个阶段被执行。

/**
 * Scheduler job interface.
 * Will be called by the scheduler.
 */
run () {
  if (this.active) {
    // 调用 this.get 求新值
    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) {
        // 侦听属性则执行用户定义的函数
        const info = `callback for watcher "${this.expression}"`
        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
      } else {
        // 执行回调函数
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}
evaluate 方法

这个方法用在计算属性的响应式 computedGetter 中,在文件 /src/core/instance/state.js 中见代码

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

对于计算属性的初始化内容前面我们有学习,计算属性的 Watcher (computedWatcher) 在实例化时 lazy 选项是为真的,那么初始化时 dirty 也为真,这就导致了在 update 和 run 方法中都不会马上调用 get 求值,而是把 dirty 设为 true,并且在引用计算属性的时候,调用计算属性的 computedGetter,这个时候 evaluate 就被调用求值了,并把 dirty 设为 false。

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

是的,没有看错,Watcher 类里面也有一个 depend 方法,这个方法与 evaluate 同样是在计算属性的 computedGetter 中被调用的(computedGetter 代码见 evaluate 方法),当 Dep.target 不为空时则调用它。

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

前面文章“计算属性与侦听属性初始化浅析”中我们研究过计算属性的响应式定义,我们看看它的图简单回顾下:
Vue2源码学习笔记 - 13.响应式原理—Watcher 类详解_第3张图片

这个 depend 中会遍历所有依赖的 Dep 并执行其 depend 方法,这有点绕。没关系,其实它的本质就是把计算属性 Watcher(computedWatcher)依赖的所有 Dep 同样也让 Dep.target 依赖之。因为在定义响应式计算属性的时候没有对应关联的 Dep 对象(不像 data 属性),所以 Dep.target 无法收集对计算属性的依赖。computedWatcher.depend() 之后,Dep.target 对于计算属性的 Dep 的依赖转移到对 computedWatcher 依赖的 Dep 上。

Vue2源码学习笔记 - 13.响应式原理—Watcher 类详解_第4张图片

Dep.target 在依赖计算属性失败后,转而依赖计算属性依赖的 Dep,这样与直接依赖计算属性是一样的,而且这样还有个好处就是当计算属性依赖的变量有改变时,Dep.target 就能收到更新通知并做必要的响应。

teardown 方法

这个方法是在组件销毁或者unwatch($watch方法返回)时调用,它的作用比较简单,就是把自身从 vm._watchers 中移除,并遍历全部依赖的 Dep,调用其 removeSub 从 Dep 的 subs 订阅列表中移除自身。

总结:

渲染函数,计算属性和侦听属性等都有需要监视变量变动的需求,Watcher 正是在这种情况下应运而生,Watcher 作为观察者,配合 Dep 一起组成响应式的核心部件。这个类代码量比较多,我们也花了很多笔墨来研究学习,我觉得这是值得的。接下来我们会把前面学的这些类串起来,学习整个响应式的核心流程与逻辑。

你可能感兴趣的:(Vue2源码学习笔记,vue.js,Vue2源码,vue源码)