大家好,我是作曲家种太阳,今天我们来手把手实现 Vue 3 响应式系统中非常核心的一部分:计算属性 computed。
Vue 3 中的 computed
是基于 Proxy 与 effect 实现的具有缓存能力的响应式属性,它的底层实现包含响应依赖收集、缓存控制与调度器机制等关键模块。下面我们一步一步来实现并理解它的原理。
💡 1. computed 的作用与特性
计算属性是基于其依赖响应式数据自动更新的"派生值",特点是:
- 懒执行:只有访问时才会执行
- 缓存机制:依赖没变就不会重新计算
- 响应式:依赖变化后会重新计算并更新依赖方
🔧 2. 实现目标与步骤规划
我们将按照以下步骤构建 computed
:
- 创建
ComputedRefImpl
类 - 实现
.value
的 getter,支持收集依赖 - 利用
ReactiveEffect
实现计算逻辑 - 引入
_dirty
控制缓存失效与更新 - 引入
scheduler
实现依赖触发逻辑 - 集成测试并解决死循环问题
🧱 3. 创建 ComputedRefImpl 类
文件路径:packages/reactivity/src/computed.ts
js
/**
* 计算属性类
*/
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
/**
* 脏:为 false 时,表示需要触发依赖。为 true 时表示需要重新执行 run 方法,获取数据。即:数据脏了
*/
public _dirty = true
constructor(getter) {
this.effect = new ReactiveEffect(getter, () => {
// 判断当前脏的状态,如果为 false,表示需要《触发依赖》
if (!this._dirty) {
// 将脏置为 true,表示
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
}
get value() {
// 收集依赖
trackRefValue(this)
// 判断当前脏的状态,如果为 true ,则表示需要重新执行 run,获取最新数据
if (this._dirty) {
this._dirty = false
// 执行 run 函数
this._value = this.effect.run()!
}
// 返回计算之后的真实值
return this._value
}
}
/**
* 计算属性
*/
export function computed(getterOrOptions) {
let getter
// 判断传入的参数是否为一个函数
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
// 如果是函数,则赋值给 getter
getter = getterOrOptions
}
const cRef = new ComputedRefImpl(getter)
return cRef as any
}
我们将 getter 包装成 ReactiveEffect
,并在 .value
中执行 run()
以计算值,并通过 _dirty
控制缓存是否失效。
🎯 4. track 与 trigger 的联动机制
computed 本质上也是一个 ref,它有自己的 dep
,并通过 trackRefValue()
和 triggerRefValue()
实现依赖收集和更新触发。
js
export function trackRefValue(ref) {
if (activeEffect) {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep)
}
}
💡 这样 computed 的依赖就能被
effect()
正确收集,并在依赖变动时更新。
⚙️ 5. ReactiveEffect 添加 scheduler
调度器 scheduler 是 computed 实现"懒更新"和"多 effect 顺序控制"的关键。
js
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
当依赖变动时(通过 reactive 的 setter -> trigger -> triggerEffect),就会进入 scheduler,标记 _dirty
为 true,等待下一次 .value
访问时重新计算。
🧪 6. 测试 computed 的响应性与缓存能力
测试代码:computed.html
js
<script>
const { reactive, computed, effect } = Vue
const obj = reactive({ name: '张三' })
const fullName = computed(() => {
console.log('computed run')
return '姓名:' + obj.name
})
effect(() => {
document.querySelector('#app').innerHTML = fullName.value
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
</script>
观察打印可以发现:
- 首次渲染执行一次 computed 逻辑
- 改变依赖后重新计算
- 多次
.value
访问不会重复执行逻辑(缓存生效)
🧩 7. 避免死循环的处理
computed 的依赖触发若未正确区分顺序可能造成死循环,我们使用两个循环分开 computed 和普通 effect:
js
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect)
}
}
这样先触发 computed,再触发依赖它的副作用函数,避免了重复依赖触发造成的递归。
验证 computed 方法
文件路径: /packages/reactivity/src/computed-cache.html
html
<!DOCTYPE html>
<html lang="en">
<body>
<div id="app"></div>
</body>
<head>
<meta charset="UTF-8">
<script src="../../dist/vue.js"></script>
</head>
</html>
<script>
const { reactive, computed, effect } = Vue
const obj = reactive({
name: '张三'
})
const computedObj = computed(() => {
console.log('计算属性执行计算');
return '姓名:' + obj.name
})
effect(() => {
document.querySelector('#app').innerHTML = computedObj.value
document.querySelector('#app').innerHTML = computedObj.value
})
setTimeout(() => {
obj.name = '李四'
}, 2000);
</script>
然后浏览器打开,就看到computed内部已经执行啦
computed 数据更新过程
● 修改依赖响应式数据(例如 obj.name = xxx)
● 会触发 setter → trigger() → triggerEffects()
● 检测 effect 是否是 computed 类型,有 scheduler,则执行 scheduler
● scheduler 中把 dirty = true,并调用 triggerRefValue(this)
● triggerRefValue 再次触发 effect → 重新执行 .value 逻辑
🧩 scheduler 的作用总结
作用 | 描述 |
---|---|
📦 延迟执行 | computed 的值只有在下次访问 .value 时才重新计算,而不是数据一改就立即执行 getter |
⛔ 避免死循环 | 避免依赖链中 computed 自己触发自己 |
⚙️ 控制依赖通知 | 通过 scheduler 控制什么时候调用 triggerRefValue,通知依赖 effect 重新执行 |
✅ 配合 _dirty | scheduler 会设置 _dirty = true,下次访问 .value 时触发真正的计算 |
小结
特性 | 实现方式 |
---|---|
缓存 | 使用 _dirty 标记控制是否重新计算 |
响应式更新 | 依赖变化触发 scheduler,重新计算并触发依赖 |
依赖收集 | .value 中 trackRefValue() 完成收集 |
多 effect 区分 | computed effect 先触发,普通 effect 后触发 |
至此,我们就完成了 Vue 3 中 computed 的完整实现逻辑!下一节我们将一起探索 watch 的底层逻辑~