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,使得再次获取计算属性值时被重新计算。