上一章:手把手搭建Vue轮子从0到1:5. Ref 模块的实现
计算属性 computed 会 基于响应式依赖被缓存,并且在依赖的响应式数据发生变化时 重新计算
创建测试实例:
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>computed</title>
<script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const { effect, reactive, computed } = Vue
const obj = reactive({
count: 0,
})
const computedObj = computed(() => {
return obj.count * 2
})
effect(() => {
document.querySelector('#app').innerHTML =
`count is: ${computedObj.value}`
})
setTimeout(() => {
obj.count++
}, 1000)
</script>
</html>
在上面的测试实例中,程序主要执行了 5 个步骤:
- 使用 reactive 创建响应性数据
- 通过 computed 创建计算属性 computedObj,并且触发了 obj 的 getter
- 通过 effect 方法创建了 fn 函数
- 在 fn 函数中,触发了 computed 的 getter
- 延迟触发了 obj 的 setter
阶段 1:初始化响应式数据与计算属性
- const obj = reactive({ count: 0 })
-
源码对应 packages/reactivity/src/reactive.ts 的 reactive 函数:
- 为 obj 创建响应式代理(Proxy),代理拦截 get(用于依赖收集)和 set(用于触发更新)操作。
- 内部通过 createReactiveObject 函数,为对象添加 __v_isReactive 标记(标识为响应式对象),并使用 baseHandlers 作为代理处理器。
- const computedObj = computed(() => obj.count * 2)
-
执行 computed 函数(packages/reactivity/src/computed.ts):
- 检测到传入的是函数(getter),创建 ComputedRefImpl 实例(cRef)。
- ComputedRefImpl 构造函数核心逻辑:
- 创建 ReactiveEffect 实例(this.effect),传入两个参数:
- getter:() => obj.count * 2(计算逻辑)。
- scheduler:() => triggerRefValue(this)(依赖变化时的调度函数)。
- 关联 effect 与计算属性:this.effect.computed = this。
- 创建 ReactiveEffect 实例(this.effect),传入两个参数:
阶段 2:注册 effect 副作用
- effect(() => { document.querySelector('#app').innerHTML = ... })
- 执行 effect 函数(packages/reactivity/src/effect.ts):
- 创建 ReactiveEffect 实例(记为 renderEffect),传入回调函数(更新 DOM 的逻辑)。
- 执行 effect.run() 方法,触发回调函数执行(初始化 DOM):
- 回调中访问 computedObj.value,触发 ComputedRefImpl 的 get value 方法:
- 执行 trackRefValue(this)(packages/reactivity/src/ref.ts):
- 收集依赖:将当前活跃的 renderEffect 记录到 computedObj 的依赖列表(deps)中,后续 computedObj 变化时会触发该 effect。
- 执行 this.effect.run()(ReactiveEffect 的 run 方法):
- 执行计算属性的 getter(obj.count * 2),此时访问 obj.count,触发响应式代理的 get 拦截器:
- 执行 track 函数(packages/reactivity/src/effect.ts):收集依赖,将 computedObj 的 effect(记为 computedEffect)记录到 obj.count 的依赖列表中
- 计算结果(0 * 2 = 0)赋值给 this._value,并返回。
- 执行计算属性的 getter(obj.count * 2),此时访问 obj.count,触发响应式代理的 get 拦截器:
- 执行 trackRefValue(this)(packages/reactivity/src/ref.ts):
- 最终 DOM 被更新为 count is: 0。
- 回调中访问 computedObj.value,触发 ComputedRefImpl 的 get value 方法:
阶段 3:1 秒后更新数据(setTimeout 逻辑)
- setTimeout(() => { obj.count++ }, 1000)
- 1 秒后执行回调,触发 obj.count 的更新:
- obj.count++ 触发响应式代理的 set 拦截器(baseHandlers 中的 set 方法):
- 执行 trigger 函数(packages/reactivity/src/effect.ts):找到 obj.count 依赖列表中的 computedEffect(计算属性的 effect)。
- obj.count++ 触发响应式代理的 set 拦截器(baseHandlers 中的 set 方法):
触发计算属性的调度器
- trigger 函数内部调用 triggerEffect 处理 computedEffect:
- 检测到 computedEffect 存在 scheduler(即 ComputedRefImpl 中定义的 () => triggerRefValue(this)),执行该调度器:
- triggerRefValue(computedObj)(packages/reactivity/src/ref.ts):遍历 computedObj 的依赖列表(即 renderEffect),调用 triggerEffect(renderEffect)。
- 检测到 computedEffect 存在 scheduler(即 ComputedRefImpl 中定义的 () => triggerRefValue(this)),执行该调度器:
触发 DOM 更新
- triggerEffect(renderEffect) 执行 renderEffect.run():
- 重新执行 effect 回调(更新 DOM),再次访问 computedObj.value: - 触发 ComputedRefImpl 的 get value,执行 this.effect.run():
- 重新执行计算属性的 getter(此时 obj.count 已变为 1,计算结果为 2)。
- DOM 被更新为 count is: 2。
- 重新执行 effect 回调(更新 DOM),再次访问 computedObj.value: - 触发 ComputedRefImpl 的 get value,执行 this.effect.run():
核心流程总结
-
初始化:reactive 创建响应式代理,computed 通过 ComputedRefImpl 关联 ReactiveEffect(含计算逻辑和调度器)。
-
依赖收集:
2.1 effect 执行时,通过 computedObj.value 的 get 触发 trackRefValue,将 renderEffect 收集为 computedObj 的依赖。
2.2 计算属性的 getter 执行时,通过 obj.count 的 get 触发 track,将 computedEffect 收集为 obj.count 的依赖。
-
数据更新:
3.1. obj.count++ 触发 set 拦截器,通过 trigger 找到 computedEffect 并执行其调度器。
3.2. 调度器通过 triggerRefValue 触发 renderEffect,重新执行回调更新 DOM。
整个过程通过 依赖收集(track) 和 更新触发(trigger) 机制,结合 ReactiveEffect 的调度逻辑,实现了数据变化到视图更新的自动联动,且计算属性的更新由其依赖的数据变化驱动,无需手动干预。