4.8 计算属性 computed 与 lazy
从「懒执行的 effect」出发,依次加上 getter 返回值、
dirty缓存与 scheduler 置脏,最终实现一个能在其它 effect 中正确响应的computed。
一、lazy:让副作用函数不立即执行
function effect(fn, options = {}) {
const effectFn = () => { /* cleanup / activeEffect / effectStack ... */ }
effectFn.options = options
effectFn.deps = []
// 只有非 lazy 时才立即执行
if (!options.lazy) {
effectFn()
}
// 把包装后的副作用函数作为返回值返回
return effectFn
}
const effectFn = effect(() => console.log(obj.foo), { lazy: true })
effectFn() // 需要手动执行
关键点:
lazy为true时不执行、并且返回effectFn。调用方由此获得「手动执行副作用函数」的能力,这正是计算属性「用到才算」的基础。
二、获取 getter 的返回值
把传给 effect 的函数看作 getter,它的返回值应当能被拿到:
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const res = fn() // 保存 getter 的返回值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res // 作为 effectFn 的返回值
}
const effectFn = effect(() => obj.foo + obj.bar, { lazy: true })
const value = effectFn() // 3
注意区分:用户传入的
fn才是真正的副作用函数,effectFn是包装层;返回值要挂在包装层上。
三、第一版 computed:只有懒计算,没有缓存
function computed(getter) {
// 把 getter 当作副作用函数,创建一个 lazy 的 effect
const effectFn = effect(getter, { lazy: true })
const obj = {
get value() {
// 读取 value 时才计算
return effectFn()
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3 —— 实际重复计算了 3 次
缺陷:只做到懒计算,没有缓存,多次访问会重复执行 getter。
四、第二版 computed:dirty 标志 + scheduler
function computed(getter) {
let value // 缓存上一次计算的值
let dirty = true // 「脏」标志:为 true 表示需要重新计算
const effectFn = effect(getter, {
lazy: true,
// 依赖变化时不直接重算,只把 dirty 置为 true
scheduler() {
dirty = true
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn() // 只有「脏」时才重新计算
dirty = false // 计算完标记为不脏,下次直接用缓存
}
return value
}
}
return obj
}
| 练习 | 结果 |
|---|---|
连续两次读取 sumRes.value | 第二次直接命中缓存 |
obj.foo++ 后再读 sumRes.value | 4(scheduler 把 dirty 置脏 → 重算) |
若没有 scheduler,
dirty会永远停在false,obj.foo变化后会一直返回过期的缓存值——scheduler 的唯一职责就是把dirty重置为true。
五、第三版 computed:修复「在 effect 中读取计算属性」
effect(() => {
console.log(sumRes.value) // 只打印一次,obj.foo 变化后不再触发 → 缺陷
})
setTimeout(() => obj.foo++, 1000)
原因:本质是 effect 嵌套。
- 计算属性内部有自己的 effect,而且是懒执行的;
- 计算属性 getter 访问响应式数据时,只会把 computed 内部的 effect 收集为依赖;
- 外层 effect 读取的是
sumRes.value(一个普通对象属性),不会被内层 getter 里的响应式数据收集到。
解决:手动补上 track 与 trigger。
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true
// 依赖的响应式数据变化时,手动触发对「value」的响应
trigger(obj, 'value')
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
// 读取 value 时手动追踪:把外层 effect 收集为 obj.value 的依赖
track(obj, 'value')
return value
}
}
track(obj, 'value')让「obj是 target、'value'是 key」这一对关系成立,因此trigger(obj, 'value')能准确找回外层 effect。这是计算属性与响应系统双向打通的关键一步。
六、演进总览
| 阶段 | 新增能力 | 解决的问题 |
|---|---|---|
lazy 选项 | 不立即执行 + 返回 effectFn | 把执行时机交给调用方 |
| getter 返回值 | effectFn() 返回计算结果 | 拿到 getter 的值 |
computed v1 | 懒计算 | 用到才计算 |
computed v2 | dirty + scheduler | 缓存计算结果,避免重复计算 |
computed v3 | track(obj, 'value') / trigger(obj, 'value') | 在其它 effect 中读取计算属性也能建立响应 |
与 Vue 2 对比:Vue 2 基于
Object.defineProperty与独立的Watcher实现计算属性;这里用effect + lazy + dirty + scheduler构建了等价能力,这也是 Vue 3computed的核心思路。
相关
- 4.7 调度执行:
scheduler、jobQueue与微任务刷新 - 4.9 watch 的实现原理:scheduler 的另一个典型应用