vue 中的 computed 属性的值只有当内部依赖的 data 属性变化时才会重新求值,这是怎么做到的呢。假设传入以下 computed :
1 2 3 4 5 computed: { ab : function ( ) { return this .a + 1 ; } }
以下是 vue 初始化的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export function initState (vm: Component ) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true ) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
initComputed 做到的事情是实例化 Watcher 以及将属性挂载到实例上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 function initComputed (vm: Component, computed: Object ) { const watchers = vm._computedWatchers = Object .create(null ) const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (process.env.NODE_ENV !== 'production' && getter == null ) { warn( `Getter is missing for computed property "${key} ".` , vm ) } if (!isSSR) { watchers[key] = new Watcher( vm, getter || noop, noop, { lazy : true } ) } if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production' ) { if (key in vm.$data) { warn(`The computed property "${key} " is already defined in data.` , vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key} " is already defined as a prop.` , vm) } } } }
可以看到 Watcher 构造器接受了一个参数 { lazy: true }
,下边进入 Watcher 介绍该参数起的作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export default class Watcher { constructor ( vm: Component, expOrFn: string | Function , cb: Function , options?: ?Object , isRenderWatcher?: boolean ) { this .vm = vm vm._watchers.push(this ) if (options) { this .deep = !!options.deep this .user = !!options.user this .lazy = !!options.lazy this .sync = !!options.sync 1efore = options.before } else { this .deep = this .user = this .lazy = this .sync = false } this .dirty = this .lazy this .value = this .lazy ? undefined : this .get() } }
此时开头假设的this.ab
的值为undefined
,这里可以看到计算属性在初始化的时候不计算其值 。然后代码执行到defineComputed(vm, key, userDef)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() 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 } Object .defineProperty(target, key, sharedPropertyDefinition) }
在createComputedGetter
中用到了watcher.dirty
,而其初始值由前面传入的参数{ lazy: true }
决定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function createComputedGetter (key ) { return function computedGetter ( ) { const watcher = this ._computedWatchers && this ._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }
然后执行Object.defineProperty(target, key, sharedPropertyDefinition)
,代码执行到这里其实是把计算属性的 setter 和 getter 创建好了。所以,计算属性如果未被消费不会计算值 ,其值始终是 undefined
。
当计算属性被用到时(例如在 render 或 methods 中),此时会触发computedGetter
,进而执行evaluate
:
1 2 3 4 function evaluate ( ) { this .value = this .get() this .dirty = false }
调用this.get()
会进行依赖收集,最后返回计算属性的值,此时终于获取到了计算属性的值。这里为什么要将this.dirty
设为false
呢,是为了实现计算属性基于它的依赖进行缓存的,也就依赖没变时,不重新计算,而是使用缓存。 回顾以下代码,在第一次获取值后,watcher.lazy
始终为false
,也就永远不会执行watcher.evaluate()
,所以这个计算属性永远不会重新求值,一直使用上一次获得的值。
1 2 3 if (watcher.dirty) { watcher.evaluate() }
那什么时候会重新求值呢,当watcher.dirty
为true
时,怎么改变这个值呢,当依赖变更的时候,即在watcher.update()
中:
1 2 3 4 5 6 7 8 9 update () { if (this .lazy) { this .dirty = true } else if (this .sync) { this .run() } else { queueWatcher(this ) } }
以开头的假设为例,当this.a
改变时,将触发dep.notify()
,然后执行该dep
中所有 watcher
的update
方法,其中就包括this.ab
的watcher.update()
(这个watcher
是在前面的watcher.depend()
中添加的),此时watcher.dirty
会被改变为true
,因此当再次取this.ab
的值时,会重新计算。
计算属性的依赖变更如何触发渲染 如果计算属性在模板或 render 函数中被使用,在触发计算watcher.update()
后,会触发渲染watcher.update()
,此时取this.ab
,会得到重新计算后的值。 但怎么保证渲染watcher.update()
在计算watcher.update()
后执行呢,因为如果在之前执行得到的this.ab
会是旧值(因为此时watcher.dirty
还为false
)。这涉及到全局的 Dep.target
状态是用一个栈 targetStack
来保存。
假设在 render 函数中使用了this.ab
:
1 2 3 render () { return <div > {this.ab}</div > }
当执行 render 函数时,此时Dep.target
指向渲染watcher
,targetStack = [渲染watcher]
,执行this.a + 1
,读取到了this.a
,触发a
的get
方法,此时Dep.target
指向计算watcher
,targetStack = [渲染watcher,计算watcher]
,调用dep.depend()
收集计算watcher
作为依赖,
1 2 3 4 5 6 depend () { if (Dep.target) { Dep.target.addDep(this ) } }
经历了这样的一个收集的流程后,此时的一些状态:
1 2 3 4 5 6 7 8 { deps : [ a的dep ], dirty : false , value : 2 , getter : ƒ ab(), lazy : true }
1 2 3 4 { subs : [ ab的计算watcher ] }
此时求值结束,回到计算watcher
的get
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 get () { pushTarget(this ) let value const vm = this .vm try { value = this .getter.call(vm, vm) } finally { popTarget() } return value }
进入this.ab
的getter
函数:
1 2 3 4 5 6 7 8 9 10 11 Object .defineProperty(vm, 'ab' , { get ( ) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } })
1 2 3 4 5 6 7 depend () { let i = this .deps.length while (i--) { this .deps[i].depend() } }
因为 ab 的计算watcher
的 deps 里保存了 a
的 dep,因此又会调用 a
的 dep.depend()
,会把渲染watcher
存放进自身的 subs 中,最后a
的 dep 如下:
1 2 3 { subs : [ ab的计算watcher,渲染watcher ] }
一切初始化已经完成,若改变a
的值,将触发 a
的 dep.notify
:
1 2 3 4 5 6 notify () { const subs = this .subs.slice() for (let i = 0 , l = subs.length; i < l; i++) { subs[i].update() } }
以上代码依次调用了计算watcher.update()
和渲染watcher.update()
,保证渲染watcher.update()
在计算watcher.update()
后执行,此时 render 函数中能获取到最新的计算属性的值。
总结
通过改变this.dirty
的值来实现缓存,当this.dirty === true
时,再次访问才会执行watcher.evaluate()
获得新值。
计算属性的依赖变更时触发计算watcher.update()
,执行this.dirty = true
,使得再次获取计算属性值时被重新计算。