在源码层面上解读computed如何进行缓存的。
在vue源码src\core\instance\state.js中会对参数computed进行初始化源代码如下:(已经进行简化处理)
const computedWatcherOptions = { lazy: true }
function initComputed(vm, computed) {
// 声明变量,在vm上添加_computedWatchers属性
const watchers = vm._computedWatchers = Object.create(null)
// 循环
for (const key in computed) {
// 保存用户设置的计算属性定义
const userDef = computed[key]
// 如果userDef是函数就将userDef赋值给ta,不是将userDef.get赋值给他
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 创建watcher,第二个参数是上方的getter方法,变换执行此方法
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions )
// 代理到实例上
defineComputed(vm, key, userDef)
}
}
可以看到在initComputed函数中首先在实例vm上参见一个空对象_computedWatchers用来保存对象的watcher实例。然后对computed进行遍历获取每一项,然后对每一项的类型进行判断如果是函数就不变动是对象就获取器get函数。获取到后就new Watcher;之后进行代理。
下面先来看new Watcher中的操作;源码在src\core\observer\watcher.js(只显示本次有用的代码)
export default class Watcher {
constructor (vm,expOrFn,cb,options,isRenderWatcher) {
this.vm = vm
// options
if (options) {
this.lazy = !!options.lazy
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.value = this.lazy
? undefined
: this.get()
}
get () {
//代码省略
return value
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
可以看到当我们此次new Watcher时只会执行constructor函数,因为我们在new Watcher时传的了computedWatcherOptions = { lazy: true }参数所以this.lazy的值为true,因此不会执行get函数,就不会将watcher保存在dep中
接下来就会执行defineComputed函数对computed中的每一项进行代理是我们页面上能直接访问。源码在src\core\instance\state.js中以下代码是在源码的基础是哪个进行了简化处理的。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed(target,key,userDef) {
//代码进行了简化处理只考虑设置get
sharedPropertyDefinition.get= createComputedGetter(key)
sharedPropertyDefinition.set = noop
Object.defineProperty(target, key, sharedPropertyDefinition)
}
代码对computed中的每一项key进行了代理,获取就会执行对应的createComputedGetter函数。到此初始化过程就完成了
接下来是渲染过程。
在页面渲染时会获取computed中的数据,就会执行其对应的createComputedGetter函数代码如下
// 当组件中有值修改就会刷新页面就会读取Computed的值就会执行此函数,
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key] //获取对应的watch
if (watcher) { //当存在时,
if (watcher.dirty) { //如果watcher.dirty为true调用watcher.evaluate()刷新watcher.value,如果为false就返回原始值
watcher.evaluate()
}
// 将读取计算属性的那个watch添加到计算属性所依赖的所有状态列表中
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
拿到对应的watcher实例,因为我们默认watcher.dirty为true就会执行watcher.evaluate()函数在上方Watcher类中。会执行get函数并且将dirty变为false。当执行get函数时会执行computed中对应的设置的函数,并且如果其中如果有vm上的数据就会执行其对应的get函数,就会将当前watcher实例保存在其对应的dep上。然后执行watcher.depend()函数,将保存了当前watcher实例的dep也保存在当前watcher实例中,即双向绑定为了解绑时在当前watcher实例中能找到dep,在dep上删除当前watcher实例。
当再次获取computed中的数据时(vm实例上的数据都没改变)因为watcher.dirty已经为false所有就会直接返回watcher.value。
当vm实例上的数据(data中的数据等等)发生改变时(computed中的数据使用过)因为都被代理拦截所有会执行对应的get函数,在给函数中会执行对应的dep.notify()会找到保存的watcher实例然后执行watcher实例的update 函数会将watcher.dirty变为true。下次获取computed中对应的数据就会执行watcher.evaluate()重复上方渲染时的操作。