这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战」
一、前情回顾 & 背景
最近这个响应式数据-initState 主要讨论的是调用 Vue.prototype._init 方法中的 initState 初始化 Vue 的响应式逻辑,主要包含以下几个部分:
initProps初始化vm.$options.propsinitMethods初始vm.$options.methodsinitData初始化vm.$options.datainitComputed初始化vm.$options.computedinitWatch初始化vm.$options.watch
在前面的 initState(1)-(5) 中我们已经完成了:initProps、initMethods、initData、initComputed 的细节讨论。所以这一篇小作文将把精力放在最后一个部分 initWatch。
二、initWatch
2.1 方法位置:
src/core/instance/state.js -> initWatch
2.2 方法参数:
vm:Vue实例,一般是组件watch:vm.$options.watch,即我们在创建Vue实例时配置的watch选项;
2.3 方法作用:
把 vm.$options.watch 配置项转换成 Watcher 实例,实现监听数据变化进而触发回调。很明确,这里我们配置的 watch 时配置一个函数,例如:
const sub = {
   watch: {
      a (newVal, oldVal) { 
          this.b = newVal + oldVal
      }
   }
}
我们想表达的意思是当 a 的值发生变化时,执行 a 对应的方法;根据我们前面说 initProps 说过 Watcher 类,它的作用是解析 expOrFn 的值并对 expOrFn 求值。Watcher 的构造函数接收 cb 参数,赋值给 this.cb,等数据更新时会调用 this.cb()。这里很显然,a 就是 expOrFn,watch.a 即属性 a 对应的回调就是 cb;
2.4 initWatch 源码
看下 initState 方法中 initWatch 的调用:
export function initState (vm: Component) {
  //...
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch) // opts.wathc 就是 vm.$options.watch 
  }
}
for in 遍历 watch 对象,根据 key 对应的 handler 的数据类型做不同处理:
如果 handler 是数组,则为每个 handler 创建一个 watcher,接收一个数组就是一个语法糖,否则直接创建 watcher;
从这里可以看得出,可以一个 key 对应多个 watcher,是一个一对多的关系;
创建 watcher 时,调用 crateWatcher 方法;
function initWatch (vm: Component, watch: Object) {
  // 遍历 watch 对象
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
2.5 createWatcher 方法
方法位置:src/core/instance/state.js -> function createWatcher
方法参数:
vm:Vue实例expOrFn:Watcher类所需要的expOrFnhandler:Watcher类所需的cb参数,当expOrFn值变化时要调用的回调options:Watcher类配置项,常见的immediate就是这个options中的一个
方法作用:
处理 handler 的兼容性,作为语法糖,抹平各种类型 handler 的差异,比如当传递字符串 handler 时,就认定 handler 是 methods 中的方法;又如传递对象时,获取对象的 handler 属性,从而确保 handler 是个函数。接着调用 vm.$watch() 方法,创建 watcher;
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 如果 handler 是对象,则获取其中的 handler 选项的值
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 如果 handler 是字符串,尝试从 methods 中获取 handler 代表的方法,获取 vm[handler]
  // 从 vm 上获取,是因为 methods 的属性都已经代理到 vm 上
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // vm 实例方法创建 watcher,为啥不是 new Watcher 了,和 computed 咋个不一样嘞?
  return vm.$watch(expOrFn, handler, options)
}
2.6 vm.$watch 方法
vm.$watch 是 Vue 的原型方法,是在 stateMixin 方法中定义,而 stateMixin 是在 instance/index.js 调用:
2.6.1 stateMixin 方法
import { initMixin } from './init'
import { stateMixin } from './state'
// import ......
// Vue 构造函数
function Vue (options) {
  this._init(options)
}
initMixin(Vue) // 定义 Vue.prototype._init 方法 (./init.js 中的方法);
stateMixin(Vue) // 定义 Vue.prototype.$watch 等
// ....
export default Vue
stateMixin 在 Vue.prototype 上定义了 $set/$delete/$watch 方法和 $data/$props 属性,但是我们这里只关注 $watch 方法;
export function stateMixin (Vue: Class<Component>) {
  // $data/$props
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  // $set/$delete
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  // $watch 
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    // 创建用户 watcher,返回 unwatch
}
2.6.2 Vue.prototype.$watch
方法参数:
expOrFn:要监听的表达式或者函数,即Watcher类需要的expOrFncb:当expOrFn值改变后要执行的回调函数options:配置watcher行为的选项,如immediate
方法作用:
- 如果接收到 
cb是个对象,做兼容处理;这里对接的不是前文中的createWatcher方法的调用,而是我们开发在代码中通过this.$watch()调用时,此时有可能传入一个对象{ handler, immediate }对象作为cb; - 此时创建 
watcher标识用户 watcher,有别于Vue自己创建的watcher,比如渲染watcher这种 - 实例化 
Watcher类 - 如果 
options中有immediate配置项,则立即调用一次cb,这个就是我们使用watcher时设置immediate后,cb会立即执行一次的原理; - 返回一个 
unwatch函数,用于取消监听;这种设计很常见,比如说redux就有类似订阅后返回取消订阅函数; 
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  // 兼容性处理,cb 是一个对象
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  // options.user 标识用户 watcher,用于区分 Vue 自己创建的 watcher,如渲染 watcher
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 如果 immediate 为 true,则立即执行一次 cb
  if (options.immediate) {
    const info = `callback for immediate watcher "${watcher.expression}"`
    // 相反这里不是收集依赖的,而是让 Dep.target 为 undefined 从而阻止依赖收集,这里为啥要阻止呢?
    pushTarget()
    invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
    popTarget() // 执行完 cb 后需要 popTarget
  }
  // 返回 unwatch 函数,用于解除监听
  return function unwatchFn () {
    watcher.teardown()
  }
}
三、computed vs watch
到这里我们已经完成了 computed 计算属性和 wach 监听器的初始化的全部工作,再次回过头来总结一下:
监听器(watch)和计算属性(computed)都是 Watcher 实例,区别体现在以下几方面:
- 
lazy求值:计算属性是lazy watcher,懒 watcher,懒体现在对于求值,lazy watcher在实例化之后并不会直接求值,而是需要用到时再调用watcher.evaluate()方法进行求值,而普通watcher则会再实例化后进行求值; - 
computed的缓存:因为lazy的原因,导致只要第一次访问计算属性时才会求值,后面多次访问不会引起多次求值,只有当其依赖的响应式数据变化时触发watcher.update()方法后才会再次重新求值;不重新求值就会返回上一次计算出来的watcher.value - 
更新方式不同:在
watcher.evaluate给lazy watcher即computed更新时也只是将watcher.dirty重新置为true,而不是直接求值,反而要等到计算属性被引用到了重新watcher.evaluate()重新求值,而这一过程只能同步进行,而普通watcher有同步和异步的更新方式; 
四、总结
本文完成了 initState() 方法中的最后一个部分—— initWatch,即初始化用户 watcher。
initWatch 调用 createWatcher 方法先抹平 handler 的差异,比如对象的 handler: { handler, immediate }、字符串的 handler: 'someMethod' 等,然后再由 createWathcer 调用 vm.$watch 这个 Vue.prototype 上的原型方法创建 watcher。
Vue.prototype.$watch 方法是在 stateMixin 中创建的,$watch 专门用来创建用户 watcher 的,并且处理了 immediate 为 true 时立即调用回调一次。