ssh的博客

Vue源码学习 计算属性computed

October 17, 2018 • ☕️ 4 min read

上一篇讲解(摘抄)了Vue响应式实现的原理,良好的设计为很多看似复杂的功能奠定了基础,使得这些功能的实际实现变得很简单。

我们先得出个结论,Watcher这个类即可以用做渲染函数的watcher, 也可以用作计算属性的Watcher,这两者在初始化和部分函数的分支都是不同的, watcher的更新核心方法是update,可以说计算属性的update是为了驱动渲染watcher的update,而渲染watcher的update是为了重新调用vm.update(vm.render())方法去更新真正的页面。

首先来看初始化函数的简化版本

initComputed

function initComputed (vm: Component, computed: Object) {
  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,
        computedWatcherOptions
    )

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

vm就是vue实例,computed就是用户定义的computed对象。

首先定义了watchers数组和vm.__computedWatchers为一个空对象

  const watchers = vm._computedWatchers = Object.create(null)

接下来遍历用户传入的computed对象,computed里面可以是

key: {
  get: ...,
  set: ...
}

的形式,也可以是

key: function() {}

的形式, 所以先取到这个getter函数,

const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get

然后为每个computed的key生成一个watcher观察者, getter就是用户传入的计算函数

watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
 )

computedWatcherOptions其实就是{ computed: true }这个对象,这会使得watcher被初始化为计算属性的watcher(下文简称计算watcher),

在watcher构造函数里有这么一段, 可以看到计算watcher的value被初始化为undefined,这说明了计算属性是惰性求值,并且计算watcher的实例下定义了this.dep = new Dep()。

if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
defineComputed(vm, key, userDef)

在这之后调用了defineComputed把计算属性的key代理到了this下面,getter就定义为createComputedGetter(key),先看看createComputedGetter做了什么。

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

条件判断语句中有两句关键的代码,我们分开来看

 watcher.depend()
 return watcher.evaluate()

watcher.depend()

这个getter函数会在渲染模板遇到{{ computedValue }}这样的值的时候触发。 这时会先取到key对应的计算watcher, 并且调用watcher的depend()方法收集依赖。

  /**
   * Depend on this watcher. Only for computed property watchers.
   */
  depend () {
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }

this.dep就是在初始化时为watcher生成的,可以思考一下在这个时候调用dep的depend会收集到什么,我们来看看dep的depend

 depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

因为正在根据template生成对应的真实dom,所以这个时候的Dep.target一定是当前组件的渲染watcher,那么其实这个dep收集到的就是渲染watcher。

到这个时候,依赖收集完成了。 那我们接下来看

return watcher.evaluate()

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

这个其实是专为计算watcher设计的求值函数,this.dirty一定是在计算watcher的情况下才为true, 这时候会把this.value调用this.get()去求值,我们来看看this.get做了什么。

  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
  }

首先调用pushTarget(this), 把计算watcher设置为现在的全局Dep.target,这样其他的dep收集依赖就会收集到计算watcher了, 然后

 value = this.getter.call(vm, vm)

这个时候的getter就会调用用户自定义的计算函数 比如

computed: {
  sum() {
     return this.a + this. b
  }
}

那么此时的getter会去调用return this.a + this. b, 而在求这个值的过程中, 又会触发a和b的dep的depend, 这个时候a和b都会收集到这个计算watcher作为依赖

那么我们之后再一些methods里写this.a = 2 这样去改变a的值, 会触发a的dep去通知计算watcher去做update, 计算watcher的update方法又会去

this.dep.notify()

触发watcher的dep的notify, 这个dep收集了渲染watcher, 这样会驱动渲染watcher去执行update()就会去重新渲染页面, 这样就达成了修改a属性去触发依赖a的视图和依赖sum的视图重新进行渲染。

 update () {
    queueWatcher(this)
  }

queueWatcher会在nextTick执行watcher.run()

run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

此时的this.cb 是渲染watcher的cb 也就是vm.update(vm.render()) 这样页面就会重新渲染,更新视图