浅羲Vue源码-12-响应式数据-initState(6)

130 阅读2分钟

这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战

一、前情回顾 & 背景

最近这个响应式数据-initState 主要讨论的是调用 Vue.prototype._init 方法中的 initState 初始化 Vue 的响应式逻辑,主要包含以下几个部分:

  1. initProps 初始化 vm.$options.props
  2. initMethods 初始 vm.$options.methods
  3. initData 初始化 vm.$options.data
  4. initComputed 初始化 vm.$options.computed
  5. initWatch 初始化 vm.$options.watch

在前面的 initState(1)-(5) 中我们已经完成了:initProps、initMethods、initData、initComputed 的细节讨论。所以这一篇小作文将把精力放在最后一个部分 initWatch

二、initWatch

2.1 方法位置:

src/core/instance/state.js -> initWatch

2.2 方法参数:

  1. vm: Vue 实例,一般是组件
  2. watchvm.$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 就是 expOrFnwatch.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

方法参数:

  1. vmVue 实例
  2. expOrFnWatcher 类所需要的 expOrFn
  3. handler: Watcher 类所需的 cb 参数,当 expOrFn 值变化时要调用的回调
  4. optionsWatcher 类配置项,常见的 immediate 就是这个 options 中的一个

方法作用:

处理 handler 的兼容性,作为语法糖,抹平各种类型 handler 的差异,比如当传递字符串 handler 时,就认定 handlermethods 中的方法;又如传递对象时,获取对象的 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.$watchVue 的原型方法,是在 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

stateMixinVue.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

方法参数:

  1. expOrFn:要监听的表达式或者函数,即 Watcher 类需要的 expOrFn
  2. cb:当 expOrFn 值改变后要执行的回调函数
  3. options:配置 watcher 行为的选项,如 immediate

方法作用:

  1. 如果接收到 cb 是个对象,做兼容处理;这里对接的不是前文中的 createWatcher 方法的调用,而是我们开发在代码中通过 this.$watch() 调用时,此时有可能传入一个对象 { handler, immediate } 对象作为 cb
  2. 此时创建 watcher 标识用户 watcher,有别于 Vue 自己创建的 watcher,比如渲染 watcher 这种
  3. 实例化 Watcher
  4. 如果 options 中有 immediate 配置项,则立即调用一次 cb,这个就是我们使用 watcher 时设置 immediate 后,cb 会立即执行一次的原理;
  5. 返回一个 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 实例,区别体现在以下几方面:

  1. lazy 求值:计算属性是 lazy watcher懒 watcher,懒体现在对于求值,lazy watcher 在实例化之后并不会直接求值,而是需要用到时再调用 watcher.evaluate() 方法进行求值,而普通 watcher 则会再实例化后进行求值;

  2. computed 的缓存:因为 lazy 的原因,导致只要第一次访问计算属性时才会求值,后面多次访问不会引起多次求值,只有当其依赖的响应式数据变化时触发 watcher.update() 方法后才会再次重新求值;不重新求值就会返回上一次计算出来的 watcher.value

  3. 更新方式不同:在 watcher.evaluatelazy watchercomputed 更新时也只是将 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 的,并且处理了 immediatetrue 时立即调用回调一次。