计算属性缓存原理
示例
new Vue({
data() {
return {
count: 1,
ourder: hello,
}
},
computed: {
sum() {
return this.count + 1
},
},
methods: {
change() {
this.count = 2
},
},
})
回顾 watcher 的流程
watcher 的的核心概念是 get 求值和 update 更新
- 在求值的时候,它会把自身,也就是 watcher 本身赋值给 Dep.target 这个全局变量
- 然后求值的过程中,会读取到响应式属性,那么响应式属性的 dep 就会收集到这个 watcher 作为依赖
- 下次响应式属性更新了,就会从 dep 中找出它收集到 watcher,触发 watcher.update() 去更新
在基本的响应式更新视图的流程中
- get 求值就是指 Vue 的组件重新渲染的函数,
- update 的时候,其实就是重新调用组件的渲染函数去更新视图
这套流程同样适用于 computed 的更新
初始化 computed
Vue 会对 options 中的每个 computed 属性也用 watcher 去包装起来,它的 get 函数显然就是要执行用户定义的求值函数,而 update 则是一个比较复杂的流程
初始化
在组件初始化的时候,会进入到初始化 computed 的函数
if (options.computed) {
initComputed(vm, opts.computed)
}
// initComputed 函数
var watchers = (vm._computedWatchers = Object.create(null))
// 依次为每个 computed 属性定义
for (const key in computed) {
const userDef = computed[key]
watchers[key] = new Watcher(
vm, // 实例
getter, // 用户传入的求值函数
noop, // 回调函数
{ lazy: true } // 声明 lazy 属性,标记 computed watcher
)
// 用户在调用 getter 函数的时候,会发生的事情
defineComputed(vm, key, userDef)
}
调用循环为每个 computed 属性生成一个计算watcher,该 watcher 保留管家你属性简化后的形式为
{
deps: [],
dirty: true, // 缓存的关键
getter: sum(), // 此处假设用户传入的函数为 sum()
lazy: true,
value: undefined
}
可以看到 value 开始为 undefined,lazy 为 true,说明它的值是惰性计算的,只有到真正在模板里去读取它的值后才会计算
接下来看看 defineComputed,它决定了用户在读取 this.sum 这个计算属性的值后会发生什么
// defineComputed 简化后的代码
Object.defineProperty(target, key, {
get() {
// 组件实例上拿到 computed watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 只有 dirty 为 true 才会重新求值
if (watcher.dirty) {
watcher.evaluate()
}
// 此处也是个关键
if (Dep.target) {
watcher.depend()
}
// 返回计算出来的值
return watcher.value
}
},
})
dirty 这个概念代表脏数据,说明这个数据需要重新调用用户传入的 sum 函数来求值,第一次在模板中读到的时候,它一定是 true,所以初始化的时候就会经历一次求值
// evaluate 函数
evaluate() {
this.value = this.get()
this.dirty = false
}
下次没有特殊情况时,再次读取到 sum 的时候,发现 dirty 为 false,则直接返回 value,这就是计算属性缓存的概念
更新
当 count 更新的时候,到底是怎么触发 sum 在页面上变更的?要从上面提到的 evaluate() 函数中的求值操作说起
Dep.target 变更为渲染watcher,此时 Dep.target 为渲染watcher,targetStack 为 [渲染 watcher]
在模板中读取 sum 变量的时候,全局的 Dep.target 应该是渲染watcher,全局的 Dep.target 状态是一个用栈 targetStack 来保存,便于前进和回退 Dep.target
// get函数
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, m)
} finally {
popTarget()
}
return value
}
刚进入 get 方法的时候,就执行 pushTarget,把 计算watcher自身置为 Dep.target,等待依赖收集,执行完 pushTarget 后
Dep.target 变更为计算watcher,此时 Dep.target 为计算watcher,targetStack 为 [渲染 watcher, 计算 watcher]
代码往下,此时会执行 getter 函数,就是用户传入的 sum 函数,执行的时候读取到了 this.count,这是一个响应式的属性,此时会触发 count 的 get 劫持
// get 简化代码
// 在闭包中,会保留对于 count 这个 key 所定义的 dep
const dep = new Dep()
// 闭包中也会保留上一次 set 函数所设置的 val
let val
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
const value = vale
// Dep.target 此时是 计算watcher
if(Dep.target) {
// 收集依赖
dep.depend()
}
return value
}
})
// dep.depend()
depend() {
// 此时 Dep.target 为 计算watcher
if(Dep.target) {
Dep.target.addDep(this)
}
}
// 计算watcher 的 addDep 函数
addDep(dep) {
// 此处做了一系列去重操作,简化掉
// 这里把 count 的 dep 也存在自身的 deps 上
this.deps.push(dep)
// 带着 计算watcher 自身作为参数,回到 dep 的 addSub 函数
dep.addSub(this)
}
// Dep
class Dep {
subs = []
addSub(sub){
this.subs.push(sub)
}
}
经历上述的收集流程后,此时的一些状态,可以看出 计算watcher 和它所依赖的响应式值的 dep,互相保留了彼此
// sum 的 计算watcher
{
deps: [count的dep],
dirty: false, // 求值完了,所以是false
value: 2,
getter: sum(),
lazy: true
}
// count 的 dep
{
subs: [sum的计算watcher]
}
收集流程执行完,执行 poptarget
Dep.target 变更为渲染watcher,此时 Dep.target 为渲染watcher,targetStack 为 [渲染 watcher]
此时 evaluate 函数执行完毕,代码继续往下,走到了下面这里
if (Dep.target) {
watcher.depend()
}
此时的 Dep.target 为 渲染watcher,所以进入到了 watcher.depend()
// watcher.depend()
depend() {
let i = this.deps.length
while(i--){
this.deps[i].depend()
}
}
由于经过前面的收集依赖过程,计算watcher的 deps 里面保存了 count 的 dep,也就是说此时又会调用 count 上的 dep.depend()
// Dep
class Dep {
subs = []
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
这次的 Dep.target 已经是 渲染watcher 了,所以这个 count 的 dep 又会把 渲染watcher 存放进自身的 subs 中
{
subs: [sum的计算watcher, 渲染watcher]
}
此时 count 更新了,再回到 count 的响应式劫持逻辑里去
const dep = new Dep()
let val
Object.defineProperty(obj, key, {
set: function reactiveSetter(newVal) {
val = newVal
// 触发 count 的 dep 的 notify
dep.notify()
},
})
// Dep
class Dep {
subs = []
notify() {
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
把 subs 里保存的 watcher 依次去调用它们的 update 方法
调用
计算watcher的 update// 计算watcher 的 update update(){ if(this.lazy) { this.dirty = true } }调用
渲染watcher的 update 这里其实就是调用vm._update(vm._render())这个函数,重新根据 render 函数生成 vnode 去渲染视图而在 render 的过程中,一定会访问到 sum 这个值,那么又回到 sum 定义的 get 函数
缓存生效的情况
只有计算属性依赖的响应式值发生更新的时候,才会把 dirty 重置为 true,这一下次读取的时候才会发生真正的计算
以开头的示例来说,当 count 发生变换的时候,sum 才会重新计算,order 发生变换的时候,sum 读取的是缓存值
总结
计算属性更新的路径是为
- 响应式的值 count 更新
- 同时通知
computed watcher和渲染 watcher更新 computed watcher把 dirty 设置为 true- 视图渲染读到 computed 的值,由于 dirty 为 ture 所以
computed watcher重新求值