Vue3响应式原理:Effect与Computed的协作机制解析
本文深入解析Vue3响应式系统中Effect与Computed的内在联系。通过测试用例揭示Effect作为副作用收集器的核心作用,阐述track与trigger如何协同工作。文章逐步推导Computed基于Lazy Effect的实现逻辑,并解析targetMap、deps等关键数据结构,帮助开发者理解Vue3响应式底层原理。
Effect在响应式系统中的角色
在深入研读Vue3响应式机制后,一个核心疑问始终萦绕:在getter与setter之间穿梭的effect究竟承担何种角色?若仅实现基础响应式,似乎无需引入effect。那么,effect存在的根本意义何在?
通过偶然发现的effect测试代码用例,这一谜团得以解开。
it('should observe basic properties', () => {
let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)
})
以下是对该段测试代码的详细解析。
- 首先声明一个普通变量dummy,在effect回调函数中,将响应式对象counter的num属性值赋给dummy。
- 接着进行断言判断,验证dummy的初始值是否等于0。
- 当将counter.num重新赋值为7时,dummy的值也随之更新为7。
这一现象引发了我的深入思考。
这是否意味着computed的实现原理与此相关?
随即查阅computed的测试用例以验证猜想。
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)
观察结果令人兴奋。
进一步确认了之前的推测。
逻辑链条完全闭合。
这种豁然开朗的感觉令人振奋。
事实证明猜想完全正确。至此,effect的本质得以厘清:顾名思义,effect意为“副作用”,它是响应式系统的副产品。每次触发getter时收集对应的effect,每次触发setter时则执行这些effect。利用这一机制,可以在响应式数据变化时执行额外操作,例如实现计算属性computed。
基于Effect实现Computed
为了更清晰地理解,我们可以尝试用effect来手动实现一个computed。
此处省略复杂的边界判断逻辑,以便更直观地展示核心实现。
function computed (fn) {
let value = undefined
const runner = effect(fn, {
// 如果lazy不置为true的话,每次创建effect的时候都会立即执行一次
// 而我们要实现computed显然是不需要的
lazy: true
})
// 为什么要使用对象的形式,是因为我们最后需要得到computed的值
// 如果不用对象的 get 方法的话我们就需要手动再调用一次 computed()
return {
get value() {
return runner()
}
}
}
// 使用起来是这样的
const value = reactive({})
const cValue = computed(() => value.foo)
value.foo = 1
console.log(cValue.value) // 1
实现看似简单,但effect的具体运行机制究竟如何?
Effect内部逻辑与数据结构
不急,我们先梳理整体逻辑脉络。
- 首先,若effect回调中访问了已响应对象的getter,该effect必须被存储起来。
- 其次,需要一个存储effect的容器。在调用effect函数时,需将当前effect放入该容器。Vue内部使用数组activeReactiveEffectStack = []来维护这一栈结构。
- 再次,每个target触发时可能关联多个effect,因此每个target需对应一个依赖收集器deps。在setter触发时,遍历deps并执行对应的effect()。
- 然而,deps不能直接挂载在target对象上,否则会导致数据冗余且不简洁。因此,Vue采用Map集合来存储target与deps的映射关系,该集合在源码中称为targetMap。
接下来明确几个核心概念。
track(追踪器):在getter调用时执行,负责建立target、key与effect之间的依赖关系。
// 比如 const react = reactive({a: { b: 2 })
// react.a 时 target -> {a: { b: 2 } key -> a
// targetMap 储存了 target --> Map --> key --> Set --> dep --> effect
// 当调用 react.a.b.c.d.e 时 depsMap
// {"a" => Set(1)} --> Set --> effect
// {"b" => Set(1)}
// {"c" => Set(1)}
// {"d" => Set(1)}
// {"e" => Set(1)}
export function track(target: any, key: string) {
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
if (effect) {
let depsMap = targetMap.get(target);
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key!);
if (!dep) {
depsMap.set(key!, (dep = new Set()));
}
if (!dep.has(effect)) {
dep.add(effect);
effect.deps.push(dep);
}
}
}
trigger(触发器):逻辑相对直观,通过target和key获取对应的所有effect,并遍历执行它们。
export function trigger(target: any, key?: string | symbol) {
const depsMap: any = targetMap.get(target);
const effects: any = new Set()
if (depsMap && depsMap.get(key)) {
depsMap.get(key).forEach((dep: any) => {
effects.add(dep)
});
effects.forEach((e: any) => e())
}
}
以下是effect函数的具体实现代码。
// 暴露的 effect 函数
export function effect(
fn: Function,
options: any = EMPTY_OBJ
): any {
if ((fn as any).isEffect) {
fn = (fn as any).raw
}
const effect = createReactiveEffect(fn, options)
// 如果不是 lazy,则会立即执行一次
if (!options.lazy) {
effect()
}
return effect
}
// 创建 effect
function createReactiveEffect(
fn: Function,
options: any
): any {
const effect = function effect(...args: any): any {
return run(effect as any, fn, args)
} as any
effect.isEffect = true
effect.active = true
effect.raw = fn
effect.scheduler = options.scheduler
effect.onTrack = options.onTrack
effect.onTrigger = options.onTrigger
effect.onStop = options.onStop
effect.computed = options.computed
effect.deps = []
return effect
}
// 执行函数,执行完之后会将储存的 effect 删除
// 这是函数 effect 的所有执行,所经历的完整的声明周期
function run(effect: any, fn: Function, args: any[]): any {
if (!effect.active) {
return fn(...args)
}
if (activeReactiveEffectStack.indexOf(effect) === -1) {
try {
activeReactiveEffectStack.push(effect)
return fn(...args)
} finally {
activeReactiveEffectStack.pop()
}
}
}
回顾全文,总结学习源码的方法:若对源码某部分感到无从下手,建议从测试用例入手。测试用例能清晰展示函数预期效果,进而推导设计意图及实现思路,结合源码与个人思考,能更高效地掌握知识。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
怎么在React或Vue前端中使用Cube.js Client获取数据
2026-11-03 09:24:09
Vue.js 新手入门:怎么使用 Vite 快速搭建第一个项目
2026-11-02 10:30:55
Vue 项目中封装 heatmap.js:生命周期管理与内存释放
2026-11-02 09:17:40
uni-app 自定义组件开发与通信机制详解
2026-11-01 02:03:46
Vue.js 项目中 Mock 数据的三种实现方案
2026-10-31 17:23:18
