Vue 中 Computed 原码解读

355 阅读4分钟

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战

前言

在Vue中,计算属性 computed 是我们经常用到的功能之一,它类似于watch,但是又有很大的区别。computedWatcher类的最后也是最复杂的一种实例化的使。下面我们通过阅读源码,来看看它的实现原理。

Watcher

computed 是响应式的数据,所以computed在初始化和使用的时候都会用到Watcher类,我们简单说下Watcher

Watcher的原理是先把自己设置到全局唯一的指定位置(windonw.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取真正读取数据的watcher,并把这个wathcer收集到Dep中去。通过这样的方式,watcher 可以主动去订阅任意一个数据的变化。

这里比较关键的就是在执行数据的getter时,会收集依赖

 Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() // 在getter中收集依赖
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
    }
  })

对于Watcher我们简单了解到这里就可以,不然掌开来讲的话篇幅就太大了。

computed 特点

computed 有两个特点:

  • 在没有使用到 computed 的时候,不会运行 computed 的 getter
  • 在改变 computed 所依赖的响应式数据的时候,不会立即更新 computed 的值,一切都要等到使用 computed 的时候

computed 原理

我们从 computed 的这两个特点出发,分别对其进行分析

第一个特点

在Vue初始化的时候会在initState中调用initComputed方法对computed进行初始化。

// ./src/core/instance/state.js
function initState(vm) {  // 初始化所有状态时
  vm._watchers = []  // 当前实例watcher集合
  const opts = vm.$options  // 合并后的属性
  
  ... // 其他状态初始化
  
  if(opts.computed) {  // 如果有定义计算属性
    initComputed(vm, opts.computed)  // 进行初始化
  }
  ...
}

我们来看下initComputed是怎么处理的

// ./src/core/instance/state.js
function initComputed(vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象
  
  for(const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        {lazy: true}
      )  // 实例化computedWatcher
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
    ...
  }
  
}

里面判断computed是函数类型还是对象类型,如果是函数则这个函数就是getter,否则使用computed.get。 获取到 getter 生成一个 Watcher,并设置lazy属性为true。这个lazy属性就是为了实现computed第一个特点。

// src/core/observer/watcher.js
class Watcher{
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this._watchers.push(this)
    
    if(options) {
      this.lazy = !!options.lazy  // 表示是computed
    }
    
    this.dirty = this.lazy  // dirty为标记位,表示是否对computed计算
    
    this.getter = expOrFn  // computed的回调函数
    
    this.value = this.lazy
      ? undefined
      : this.get();
  }
}

根据最后一句我们可以知道,如果lazy属性为true则不会直接调用get方法,所以才会在没有使用到 computed 的时候,不会运行 computed 的 getter

第二个特点

在生成了 computedWatcher之后,会继续执行defineComputed

function defineComputed(target, key, userDef) {
  ...
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: createComputedGetter(key),
    set: userDef.set || noop
  })
}

执行 defineComputed方法,通过Object.defineProperty把computed变成响应式数据。
createComputedGetter中,对getter进行封装

function createComputedGetter (key) {
  return function () {  // 返回getter函数
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 原来this还可以这样用,得到key对应的computed-watcher
    
    if (watcher) {
      if (watcher.dirty) {  // 在实例化watcher时为true,表示需要计算
        watcher.evaluate()  // 进行计算属性的求值,调用传过去的 computed.get方法
      }
      if (Dep.target) {  // 当前的watcher,这里是页面渲染触发的这个方法,所以为render-watcher
        watcher.depend()  // 收集当前watcher
      }
      return watcher.value  // 返回求到的值或之前缓存的值
    }
  }
}

------------------------------------------------------------------------------------

class Watcher {
  ...
  evaluate () {
    this.value = this.get()  //  计算属性求值
    this.dirty = false  // 表示计算属性已经计算,不需要再计算
  }
  depend () {
    let i = this.deps.length  // deps内是计算属性内能访问到的响应式数据的dep的数组集合
    while (i--) {
      this.deps[i].depend()  // 让每个dep收集当前的render-watcher
    }
  }
}

在使用了computed数据后,会调用getter方法,computed 所对应的 Watcher 会被收集,所以才会在里面的响应式数据改变后,computed计算属性跟着改变。

到这里我们就把computed的原理大致讲完了,如果需要完全理解,可能需要对vue数据响应原理有个较深的认识,比如DepWatcher等。

computed 缓存机制

computed的计算属性还具有有缓存机制,只有当其依赖的响应式数据发生变化时才会清空缓存重新计算结果,其实上面的分析已经有了,就是通过 dirty属性,只有dirty为true时才会重新计算结果替换缓存。dirty只有当其响应式数据发送变化时才会设置为true,重新计算后会再次被设置为false。

if (watcher.dirty) {  // 在实例化watcher时为true,表示需要计算
    watcher.evaluate()  // 进行计算属性的求值,调用传过去的 computed.get方法
}

总结

我们对上面这个流程做个总结。

  1. 在初始化Vue的时会执行initComputed,在里面生成lazy属性为true的WatcherWatcher根据lazy属性不立即执行get方法。
  2. 然后执行 defineComputed方法,通过Object.defineProperty把computed变成响应式数据。
  3. 在使用了computed数据后,会调用getter方法,computed 所对应的 Watcher 会被收集,所以才会在里面的响应式数据改变后,computed计算属性跟着改变。