浅羲Vue源码-11-响应式数据-initState(5)

255 阅读3分钟

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

一、前情回顾 & 背景

在上一篇小作文中,我们详述了 new Vue 实现数据响应式的方法 initState 方法中继 initProps 方法后的 initMethodsinitDatainitComputed

但是由于篇幅的问题,initComputed 有一部分细节移到本篇中,所以本篇的重点还是 initComputed。我们回忆一下 initComputed 方法都做了什么:

  1. 创建 vm._computedWatchers 对象,这个对象用于保存用 computed 创建的 watcher,每个计算属性都有
  2. 遍历 computed,即 vm.$options.computed,给每个 key 都用 new Watcher 创建一个 watcher,并且保存早 vm._computedWatchers
  3. 调用 defineComputed(vm, key, userDef) 方法将每个 key 代理到 vm 上,每个 key 就是一个计算属性
  4. 判重处理,与 props、methods、data 中的 key 对比,不能出现重复的 key

二、defineComputed 方法

方法位置:src/core/instance/state.js -> defineComputed

方法参数:

  1. target 对象,接收到的是 vm
  2. key 属性,接收到的是 computed 中的 key
  3. useDef,用户定义 computed key 对应的定义对象或者函数,当然在这里收到一个函数

方法作用:将 computed 上的 key 访问代理到 vm 上;

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering() // 非 ssr,shouledCache 为 true

  // 根据 userDef 参数构造 computed 的配置对象,包含 get 和 set
  // computed 的定义有两种方式:someComputed () { return this.xx + this.aa }
  // 或者 someComputed: { get () { return this.xx + this.zz} }
  // 当 userDef 为函数时就是第一种,else 就是第二种
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  
  // ....省去一些不重要代码
  
  // 拦截 target.key 的访问和设置
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

三、createComputedGetter 方法

3.1 方法基础

方法位置:src/core/instance/state.js -> function createComputedGetter

方法参数:computed 中的 key

方法作用:根据给定 key 返回一个作为该计算属性的 getter 函数;在这个函数中主要做了以下事项:

  1. 用接收到的计算属性的 keythis._computedWatchers 中找到其对应的 watcher
  2. 如果取到并且 watcher.dirtytrue,注意这个 watcher.dirty,则调用 watcher.evaluate() 方法求值。
  3. 如果 Dep.target 有就调用 watcher.depend()
    • 啥意思呢?表示 Dep.target 所代表的 watcher 要收收集当前这个 computed watcher 所有的依赖。听起来很拗口,我来解释一下: 比如有几个计算属性,computed: { a(){ this.b + this.c }, b,c },就是说 abc两个计算属性之和,此时 a 被渲染到模板上。在渲染的时候 Dep.target 就是渲染 watcher,而 a 对应的计算属性 watcher 依赖了 bc,此时,Dep.target 代表的渲染 watcher 就要收录 b、c,以实现 b/c 变化时能重新渲染
  4. 返回 watcher.value 属性,value 是第二步 evaluate 的结果;
function createComputedGetter (key) {
  return function computedGetter () {
    // 得到当前 key 对应的 watcher
    // this._computedWatchers 是在上面 initComputed 的时候添加到 vm 实例上的,vm 即 this
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
     
      if (Dep.target) {
        watcher.depend()
      }
      // 调用 watcher.evaluate 就是求值,求 watcher 接收到的 expOrFn 的值
      return watcher.value
    }
  }
}

3.2 computed 的缓存问题

相信很多人都听过 watchercomputed 的区别之一就是 computed 是有缓存的,可能曾经你觉得它高深莫测,今天你就发现它就是一个标识符;

举个例子吧,我们 test.html 中定义了一个 someComputed 计算属性,假如我们在模板是这样的:

<div> {{ someComputed }} </div>
<div> {{{ someComputed }} </div>

接下来我们按流程分析,当 渲染 watcher 求值的时候,就会读取到 vm.someComputed,而 vm.someComputed 已经被 Object.defineProperty 所拦截,读取时将会调用前面其描述对象的 getter,而这个 getter 就是上面 3.1 createComputedGetter() 方法返回的函数,如下:

function createComputedGetter (key) {
  return function computedGetter () {
      // 这个就是 上面文字说的 getter 函数
    }
  }
}

这个 getter 执行就会执行 watcher.evaluate() 方法求值,但你会发现 computedGetter 函数中有一个判断: if(watcher.dirty) { watcher.evaluate() }

第一次执行的时候没有问题,watcher.dirty = options.lazy === true 的,但是 watcher.evaluate 的逻辑就会吧 watcher.dirty 赋值为 false

关键就来了,上面一共会访问两次 someComputed 这个计算属性,第一次 watcher.dirtytrue 执行 watcher.evaluate() 求值,但是第二次访问 someComputed 时,此时 watcher.dirty 变为了 false 就不会再调用 watcher.evaluate() ,而是直接返回了 watcher.value,此时 watcher.value 还是上一个被访问时求得的值,这所谓 computed 缓存;

既然有缓存,那么什么时候才能触发重新求值呢?当然是 watcher.dirty 变为 true 的时候,当 watcher.update 方法被调用的时候就会把 watcher.ditry 重新置为 true

四、Wather 原型方法和 computed

4.1 watcher.prototype.evaluate()

该方法仅用于对 watcher.lazytruewatcher 进行求值。

所谓求值就是求创建 Watcher 实例传入的 expOrFn 的值。在 initComputed 时,我们传给 WatcherexpOrFnvm.$options.computed[key] 代表的求解计算属性的 getter 函数,因为 lazy,所以在创建 Watcher 后得到 watcher.value 是个 undefined`。

export default class Watcher {
  // ... public properties
  constructor () {
  }

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
}

4.2 Watcher.prototype.get

维护依赖收集时的 Dep.target 属性,然后调用通过解析 expOrFn 得来的 this.getter 方法,this.getterthis 绑定为 vm

this.getter 执行过程中,伴随着很多的响应式数据的访问,就会触发前面 defineReactive 定义响应式数据的 getter,在这些 getter 中有一个 if(Dep.target) { dep.depend() },此时 Dep.target 的值就是当前 Watcher 实例 本身,dep 就会把这个 watcher 收录起来;

export default class Watcher {
  // ... public properties
  constructor () {
  }

  get () {
    // 维护 Dep.target,Dep.target = this,this 是 Watcher 实例
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行回调函数,比如 updateComponent,进入 patch 阶段
      value = this.getter.call(vm, vm)
    } catch (e) {
     // 错误处理
    } finally {
      // 当 deep 为 true 时,深度 watch
      if (this.deep) {
        traverse(value)
      }
      // 维护 Dep.target, Dep.target = null,
      // 当前 watcher 求值结束,依赖收集也结束 
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}

4.3 Watcher.proptotype.update

当响应式数据发生改变时,即触发 setter 时,会由 dep.notify() 派发更新,dep 会找到自己收集的 watcher 们开工,调用各自的 update 用户触发更新。

initComputed 的过程中,update 的一个十分显著的作用就是重置 watcher.dirty 属性为 true,即 wather.lazytrue 的情况。这个操作使得计算属性再次被读取时调用 watcher.evaluate() 方法重新求值。这里算是和前的 3.2 comptued 的缓存问题 的一个呼应。

当有 watcher.synctrue,即需要同步更新,调用 watcher.run() 方法,这个不展开

最后一个作为压轴出演,即常规的队列更新,这个队列更新是个异步队列,其实从这里大家能感受到,watchercomputed 的另一个区别:computed 只能放同步逻辑,而 watcher 是可以处理异步逻辑的,说到底其实是两者的更新方式不同。

export default class Watcher {
  // ... public properties
  constructor () {
  }
  
  update () {
    if (this.lazy) {
      // 懒执行时走这里,比如 computed
      // 将 dirty 置为 true,就可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
      this.dirty = true
    } else if (this.sync) {
      // 在使用 $watch 或者 watch 选项时,可以传入一个 sync 选项,标识 watcher 需要同步更新
      this.run()
    } else {
      // 一般的 watcher 更新都是异步队列,将 watcher 放入到更新对象队列
      queueWatcher(this)
    }
  }
}

五、总结

本文作为 initComputed 的收尾篇,主要讨论了以下问题:

  1. 详述了 defineComputed 方法,它将 crateComptuedGetter 返回的 函数作为描述对象的 getter,然后调用 Object.defineProperty 将计算属性代理到 vm 上;
  2. createComputedGetter 方法,它返回一个函数,这个函数将是计算属性取值时的 getter 函数,这个函数获取计算属性对应的 watcher,根据 watcher.dirty 是否调用 watcher.evaluate() 还是直接使用 watcher.value 这个缓存值;
  3. computed 缓存的原理,已经重新求值的办法,核心在 watcher.dirty 这个属性
  4. 最后讨论了 Wathcer.prototype 上的 evaluate/get/update 方法的细节