两个月前我曾在掘金翻译了一篇关于Vue中简单介绍computed
是如何工作的文章,翻译的很一般所以我就不贴地址了。有位我非常敬佩的前辈对文章做了评价,内容就是本文的标题“感觉原文并没有讲清楚 computed 实现的本质- lazy watcher”。上周末正好研究一下Vue的源码,特意看了computed
,把自己看的成果和大家分享出来。
Tips:如果你之前没有看过Vue的源码或者不太了解Vue数据绑定的原理的话,推荐你看我之前的一篇文章简单易懂的Vue数据绑定源码解读,或者其他论坛博客相关的文章都可以(这种文章网上非常多)。因为要看懂这篇文章,是需要这个知识点的。
一. initComputed
首先,先假设传入这样的一组computed
:
//先假设有两个data: data_one 和 data_two
computed:{
isComputed:function(){
return this.data_one + 1;
},
isMethods:function(){
return this.data_two + this.data_one;
}
}
复制代码
我们知道,在new Vue()
的时候会做一系列初始化的操作,Vue中的data,props,methods,computed
都是在这里初始化的:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) //初始化props
if (opts.methods) initMethods(vm, opts.methods) //初始化methods
if (opts.data) {
initData(vm) //初始化data
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) //初始化computed
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch) //初始化initWatch
}
}
复制代码
我在数据绑定的那边文章里,详细介绍了initData()
这个函数,而这篇文章,我则重点深入initComputed()
这个函数。
const computedWatcherOptions = { lazy: true } //用于传入Watcher实例的一个对象
function initComputed (vm: Component, computed: Object) {
//声明一个watchers,同时挂载到Vue实例上
const watchers = vm._computedWatchers = Object.create(null)
//是否是服务器渲染
const isSSR = isServerRendering()
//遍历传入的computed
for (const key in computed) {
//userDef是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
)
}
//如果不是服务端渲染的,就创建一个Watcher实例
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
//如果computed中的key没有在vm中,通过defineComputed挂载上去
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
//后面都是警告computed中的key重名的
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)
}
}
}
}
复制代码
在initComputed
之前,我们看到声明了一个computedWatcherOptions
的对象,这个对象是实现"lazy Watcher"的关键。
接下来看initComputed
,它先声明了一个名为watchers的空对象,同时在vm上也挂载了这个空对象。之后遍历计算属性,并把每个属性的方法赋给userDef
,如果userDef
是function的话就赋给getter
,接着判断是否是服务端渲染,如果不是的话就创建一个Watcher
实例。Watcher
实例我也在上一篇文章分析过,就不逐行分析了,不过需要注意的是,这里新建的实例中我们传入了第四个参数,也就是computedWatcherOptions
,这时,Watcher
中的逻辑就有变化了:
//这段代码在Watcher类中,文件路径为vue/src/core/observer/watcher.js
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
复制代码
这里的options指的就是computedWatcherOptions
,当我们走initData
的逻辑的时候,options
并不存在,所以this.lazy = false
,但当我们有了computedWatcherOptions
后,this.lazy = true
。同时,后面还有这样一段代码:this.dirty = this.lazy
,dirty
的值也为true
了。
this.value = this.lazy
? undefined
: this.get()
复制代码
这段代码我们可以知道,当lazy
为false
时,返回的是undefined
而不是this.get()
方法。也就是说,并不会执行computed
中的两个方法:(请看我开头写的computed示例)
function(){
return this.data_one + 1;
}
function(){
return this.data_two + this.data_one;
}
复制代码
这也就意味着,computed
的值还并没有更新。而这个逻辑也就暂时先告一段落。
二. defineProperty
让我们再回到initComputed
函数中来:
if (!(key in vm)) {
//如果computed中的key没有在vm中,通过defineComputed挂载上去
defineComputed(vm, key, userDef)
} 复制代码
可以看到,当key值没有挂载到vm上时,执行defineComputed
函数:
//一个用来组装defineProperty的对象
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
//是否是服务端渲染,注意这个变量名 => shouldCache
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
//如果userDef是function,给sharedPropertyDefinition.get也就是当前key的getter
//赋上createComputedGetter(key)
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
//否则就使用userDef.get和userDef.set赋值
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
//最后,我们把这个key挂载到vm上
Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码
defineComputed
中,先判断是否是服务端渲染,如果不是,说明计算属性是需要缓存的,即shouldCache
是为true
。接下来,判断userDef
是否是函数,如果是就说明是我们常规computed
的用法,将getter
设为createComputedGetter(key)
的返回值。如果不是函数,说明这个计算属性是我们自定义的,需要使用userDef.get
和userDef.set
来为getter
和setter
赋值了,这个else部分我就不详细说了,不会到自定义computed
的朋友可以看文档计算属性的setter。最后,将computed
的这个key挂载到vm上,当你访问这个计算属性时就会调用getter。
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
}
}
}复制代码
最后我们来看createComputedGetter
这个函数,他返回了一个函数computedGetter
,此时如果watcher
存在的情况下,判断watcher.dirty
是否存在,根据前面的分析,第一次新建Watcher
实例的时候this.dirty
是为true
的,此时调用watcher.evaluate()
:
function evaluate () {
this.value = this.get()
this.dirty = false
}复制代码
this.get()
实际上就是执行计算属性的方法。之后将this.dirty
设为false
。另外,当我们执行this.get()
时是会为Dep.target
赋值的,所以还会执行watcher.depend()
,将计算属性的watcher
添加到依赖中去。最后返回watcher.value
,终于,我们获取到了计算属性的值,完成了computed
的初始化。
三. 计算属性的缓存——lazy Watcher
不过,此时我们还并没有解决本文的重点,也就是"lazy watcher"。还记得Vue官方文档是这样形容computed
的:
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是 计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要message
还没有发生改变,多次访问reversedMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
回顾之前的代码,我们发现只要不更新计算属性的中data属性的值,在第一次获取值后,watch.lazy始终为false,也就永远不会执行watcher.evaluate(),所以这个计算属性永远不会重新求值,一直使用上一次获得(也就是所谓的缓存)的值。
一旦data属性的值发生变化,根据我们知道会触发update()
导致页面重新渲染(这部分内容有点跳,不清楚的朋友一定先弄懂data数据绑定的原理),重新initComputed
,那么this.dirty = this.lazy = true
,计算属性就会重新取值。
OK,关于computed的原理部分我就说完了,不过这篇文章还是留了个坑,在createComputedGetter函数中有这样一行代码:
const watcher = this._computedWatchers && this._computedWatchers[key]复制代码
根据上下文我们可以推测出this._computedWatchers中肯定保存着initComputed时创建的watcher实例,但什么时候把这个实例放到this._computedWatchers中的呢?我还没有找到,如果有知道的朋友请留言分享,大家一起讨论,非常感谢!