Vue3 响应式系统中 effect 的 lazy 与 computed 的延迟计算与缓存机制
字数 2377 2025-12-09 23:42:12
Vue3 响应式系统中 effect 的 lazy 与 computed 的延迟计算与缓存机制
描述
在 Vue3 的响应式系统中,effect 函数和 computed 计算属性都基于副作用调度机制,但它们在执行时机和缓存策略上有显著差异。effect 默认立即执行(非 lazy),而 computed 则具有延迟计算(lazy evaluation)和值缓存(caching)的特性。理解这两者的内部协作机制,对于掌握响应式系统的性能优化和正确使用计算属性至关重要。
解题过程
1. 基础概念回顾
- effect:一个包装了用户自定义函数的副作用函数,当它依赖的响应式数据变化时会被重新执行。
- computed:一个基于响应式数据派生出的计算属性,它返回一个通过 getter 函数计算出的值,并具有缓存和响应式更新的能力。
2. effect 的 lazy 选项
在源码中,effect 函数接收两个参数:fn(副作用函数)和 options(配置对象)。其中 options.lazy 默认为 false。
// 简化版源码逻辑
function effect(fn, options = {}) {
// 创建副作用对象 effect
const effectFn = () => {
// 执行原始函数 fn
fn()
}
// 将配置项挂载到 effect 对象上
effectFn.options = options
// 如果不是 lazy,立即执行一次副作用函数
if (!options.lazy) {
effectFn()
}
// 返回 effect 函数(注意:这里返回的是包装后的 effectFn,而不是原始 fn)
return effectFn
}
- 非 lazy(默认):创建
effect后立即执行一次fn,用于首次依赖收集。 - lazy 模式:创建后不立即执行,而是将
effectFn返回,允许开发者手动控制何时首次执行。
3. computed 的延迟计算(lazy evaluation)原理
computed 内部正是利用了 effect 的 lazy: true 选项来实现延迟计算。
// 简化版 computed 实现
function computed(getter) {
let value // 缓存的值
let dirty = true // 脏数据标志,为 true 表示需要重新计算
// 1. 创建 lazy 的 effect,包装 getter 函数
const effectFn = effect(getter, {
lazy: true, // 关键:延迟执行
scheduler() {
// 当依赖变化时,不直接重新计算值,而是标记为脏数据
dirty = true
// 这里可以触发依赖此 computed 的 effect 重新执行(例如模板渲染)
}
})
const obj = {
get value() {
// 2. 只有当 dirty 为 true 时才重新计算
if (dirty) {
value = effectFn() // 手动执行 effectFn,获取最新计算值
dirty = false // 计算完成,标记为干净数据
}
// 3. 依赖收集:当其他 effect 读取 computed.value 时,会建立依赖关系
return value
}
}
return obj
}
关键点:
- 首次访问才计算:
computed创建时不会立即执行getter,只有当第一次访问.value属性时才会计算。 - 脏检查机制:
dirty标志初始为true,表示未计算或需要重新计算。当依赖的响应式数据变化时,scheduler被触发,仅将dirty设为true,并不立即重新计算值。 - 缓存:只要
dirty为false,后续访问.value都直接返回缓存的值,不会重新执行getter。
4. 依赖收集与更新的完整流程
假设有以下代码:
const count = ref(0)
const double = computed(() => count.value * 2)
// 创建一个 effect 依赖 double
effect(() => {
console.log(double.value)
})
count.value = 1 // 触发更新
步骤分析:
步骤1:创建 computed
computed内部创建 lazy effecteffectFn,但不执行getter。- 此时
dirty = true,value未定义。
步骤2:effect 首次执行
effect(() => console.log(double.value))立即执行。- 读取
double.value→ 触发get value()。 - 由于
dirty = true,执行effectFn()(即() => count.value * 2)。 - 执行过程中读取
count.value,count收集到computed内部的effectFn作为依赖。 - 计算结果
0存入value,dirty = false,返回0。 - 同时,
double.value的 getter 被effect读取,double收集到外部的effect作为依赖。
步骤3:依赖更新
count.value = 1触发count的 setter。count触发它收集的依赖:computed内部的effectFn的scheduler。scheduler执行:仅设置dirty = true,不重新计算值。- 同时,
scheduler会触发double自己的依赖(外部的effect)。
步骤4:effect 重新执行
- 外部
effect被触发,再次执行console.log(double.value)。 - 再次读取
double.value,此时dirty = true,重新执行effectFn()计算得到2。 - 更新
value缓存,dirty = false,返回2。
5. 性能优势
- 避免不必要的计算:如果
computed依赖的数据变化了,但没有人读取.value,就不会进行实际计算。 - 缓存减少开销:多个地方访问
computed只会计算一次,后续直接返回缓存值。 - 调度控制:
scheduler允许在依赖变化时执行自定义逻辑(如标记脏数据),而不是立即重新计算。
6. 与普通 effect 的对比
| 特性 | effect (非 lazy) |
computed |
|---|---|---|
| 首次执行 | 立即执行 | 延迟到首次访问 .value |
| 缓存 | 无,每次依赖变化都重新执行 | 有,依赖变化后首次访问才重新计算 |
| 返回值 | 无返回值(可手动返回) | 返回一个具有 .value 的 ref 对象 |
| 调度控制 | 可通过 scheduler 自定义 |
内置 scheduler 用于标记脏数据 |
总结
computed 通过 effect 的 lazy: true 选项实现了延迟计算,结合 dirty 标志和缓存机制,确保只有在需要时才进行昂贵的计算。这种设计既保证了响应性,又避免了不必要的性能开销。理解这一机制有助于在开发中正确使用计算属性,避免将其误当作普通函数使用,从而充分发挥 Vue3 响应式系统的性能优势。