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()) 这样页面就会重新渲染,更新视图