Vue.js 3 渐进式实现之响应式系统——第十节:计算属性与缓存

110 阅读3分钟

往期回顾

  1. 系列开篇与响应式基本实现
  2. effect 函数注册副作用
  3. 建立副作用函数与被操作字段之间的联系
  4. 封装 track 和 trigger 函数
  5. 分支切换与 cleanup
  6. 嵌套的 effect 与 effect 栈
  7. 避免无限递归循环
  8. 调度执行
  9. 懒执行的 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 都会重新执行,就像普通的响应式依赖一样,目前我们的实现还做不到这一点。

下一节我们将实现这个功能。