往期回顾
- 系列开篇与响应式基本实现
- effect 函数注册副作用
- 建立副作用函数与被操作字段之间的联系
- 封装 track 和 trigger 函数
- 分支切换与 cleanup
- 嵌套的 effect 与 effect 栈
- 避免无限递归循环
- 调度执行
- 懒执行的 effect
计算属性与缓存
目前我们的响应式系统已经可以支持调度执行和懒执行了,在此基础上本节我们开始实现计算属性。
思路
计算属性
function computed(getter) {
// 把 getter 作为副作用函数,创建一个 lazy 的 effect
const effectFn = effect(getter, { lazy: true })
const obj = {
// 当读取 value 时,才会执行 effectFn
get value() {
return effectFn()
}
}
return obj
}
首先我们定义一个 computed 函数,它接收一个 getter 作为参数。我们把 getter 作为一个副作用函数创建一个懒执行的 effect。computed 函数返回一个对象,该对象的 value 属性是一个访问器属性,只有读取 value 时才会执行 effectFn 进而获取到传入的 getter 的返回值。
缓存
目前我们的计算属性只做到了懒计算,也就是只有真正读取返回的 obj 的 value 属性时才会计算并得到 getter 的返回值,但是还做不到缓存。
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })
const sum = computed(() => obj.foo + obj.bar)
console.log(sum.value) // 3
console.log(sum.value) // 3
console.log(sum.value) // 3
上面的代码多次访问 sum.value 的值,每次都会重新调用 effectFn 计算,哪怕 obj.foo 和 obj.bar 的值都没有发生变化,这显然是没有必要的。
缓存指的是,只有当 getter 依赖的响应式数据有变化时,读取计算属性才会重新计算,否则读到的都是上一次计算的结果。
解决这个问题,我们需要为 computed 函数添加缓存功能:
- 每个计算属性自身维护一个变量,存储上次计算的结果
- 读取计算属性时:
- 如果上次计算后依赖的响应式数据没有更新,直接返回缓存
- 如果有更新,重新计算并更新缓存再返回
function computed(getter) {
// value 用来缓存上一次计算的值
let value
// dirty 标志,用来标识是否需要重新计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() {
// 依赖的响应式数据有变化,需要重新计算
dirty = true
}
})
const obj = {
get value() = {
// 只有 dirty 为 true 时,才需要重新计算,并更新缓存 value
if (dirty) {
value = effectFn()
// 将 dirty 设置为 false,表示已经计算过了,不再是脏值了
dirty = false
}
return value
}
}
return obj
}
我们为 effect 添加了 scheduler 调度器函数,它会在 getter 依赖的响应式数据变化时执行。我们正利用了这一点,在 scheduler 函数中把 dirty 置为 true。这样只要 getter 的依赖有变化,下一次访问计算属性时就会重新计算。
已实现
我们利用响应式系统的调度执行和懒执行特性,实现了计算属性,并且带有缓存功能。
缺陷/待实现
当另一个 effect 中读取了计算属性的值时,我们希望每当计算属性的值有变化时,effect 都会重新执行,就像普通的响应式依赖一样,目前我们的实现还做不到这一点。
下一节我们将实现这个功能。