vue3响应式总结
在 Vue3 的响应式系统中,所有与响应式数据关联的副作用函数确实会被存储在 targetMap
结构中,它们的依赖关系建立遵循统一的机制。以下是具体分类说明:
核心概念关系图
全局 WeakMap
┌───────────┐
│ targetMap │
└─────┬─────┘
│
Key: 响应式对象 (target)
Value: depsMap (Map 结构)
│
├── Key: 属性键 (如 'count')
│ │
│ └── Value: dep (Set 集合)
│ │
│ └── 收集所有依赖该属性的 effect
│
└── Key: 另一个属性键 (如 'message')
│
└── Value: 对应 dep 集合
所有会触发依赖收集的情况
1. effect()
import { effect, reactive } from 'vue'
const state = reactive({ count: 0 })
effect(() => {
console.log(state.count) // 触发 track,收集到 targetMap[state]['count']
})
2. watch()
watch(
() => state.count, // getter 函数访问响应式属性
(newVal) => { /* ... */ }
)
// 内部创建 effect,会被收集到 targetMap[state]['count']
3. watchEffect()
watchEffect(() => {
console.log(state.count) // 直接访问,立刻触发收集
})
// 内部就是一个自动运行的 effect
4. computed()
const double = computed(() => state.count * 2)
// 计算属性的 getter 会被包裹成 effect,访问 state.count 时被收集
5. 模板渲染(最重要但最隐蔽)
<template>
<div>{{ state.count }}</div> <!-- 编译后的渲染函数访问 state.count -->
</template>
- 每个组件实例都有一个渲染 effect,当模板中访问响应式数据时,会触发依赖收集
- 等效于
effect(() => renderComponent())
6. 其他
- 组件的
setup()
函数中直接访问响应式数据 - 生命周期钩子中访问响应式数据(若发生在响应式上下文中)
响应式系统完整流程
- 访问数据:当副作用函数(effect、watch、模板等)读取响应式对象的属性时
- 触发
track()
:内部通过 Proxy 的get
拦截器调用track
函数 - 追踪依赖:将当前活跃的
ReactiveEffect
实例添加到对应属性的dep
集合 - 变化触发:当修改属性时,触发
trigger()
并找到相关的dep
集合,遍历执行其中的副作用
关键验证方式
可以通过调试工具观察 targetMap
的结构:
// 在浏览器中临时暴露 targetMap(仅调试用)
window.__VUE__ = { targetMap }
然后访问响应式数据,观察对应属性下 dep
集合中的 effect 数量。
特殊说明
- 自动解绑机制:组件卸载时,其对应的 effect 会自动从
dep
中被移除,避免内存泄漏 - 嵌套 effect:使用
effectScope
管理的 effect 会形成层级关系,便于批量处理
所有通过 Vue3 响应式 API 建立的副作用都会被纳入 targetMap
的统一管理,这也是 Vue3 实现精确更新的核心机制。
在 Vue3 的响应式系统中,targetMap
是核心依赖关系存储结构,通过结合源码中的具体实现,我们可以深入理解不同场景下副作用函数的收集机制。以下是逐层分解的源码级解析:
关键源码结构
1. 核心数据结构定义(effect.ts
)
// 响应式系统的全局存储结构
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
export const targetMap = new WeakMap<any, KeyToDepMap>()
// 当前活跃的 effect 栈
let activeEffect: ReactiveEffect | undefined
const effectStack: ReactiveEffect[] = []
二、副作用函数追踪全流程
1. 副作用封装:ReactiveEffect
类(源码核心)
// packages/reactivity/src/effect.ts
export class ReactiveEffect<T = any> {
deps: Dep[] = [] // 记录所有依赖本 effect 的 dep 集合
constructor(
public fn: () => T, // 副作用函数本体
public scheduler?: () => void // 调度器(用于 watch/computed)
) {}
run() {
if (!effectStack.includes(this)) { // 防止同一个 effect 重复入栈
try {
effectStack.push((activeEffect = this)) // 压栈并设置 activeEffect
return this.fn() // 执行副作用函数,触发 track
} finally {
effectStack.pop() // 出栈
activeEffect = effectStack[effectStack.length - 1] // 恢复之前的 effect
}
}
}
stop() { /* 从所有 dep 中移除本 effect */}
}
关键生命周期:当 effect.run()
执行时,activeEffect
指向当前实例,函数内部的响应式数据访问会触发依赖收集。
2. 依赖收集:track()
函数
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (activeEffect === undefined) return // 没有活跃 effect 时直接返回
// 获取或创建 target 对应的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取或创建 key 对应的 dep 集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 建立 effect 与 dep 的双向关联
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep) // effect 也记录 dep
}
}
触发时机:当通过响应式 Proxy 的 get
拦截器访问属性时,调用 track(target, 'get', key)
。
三、具体场景的源代码实现
1. 基础 effect()
函数
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
const _effect = new ReactiveEffect(fn) // 创建 ReactiveEffect 实例
_effect.run() // 立即运行,触发 track
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect // 关联 runner 与 effect
return runner
}
2. 计算属性 computed()
(computed.ts)
export function computed<T>(
getter: () => T
): ComputedRef<T> {
const _effect = new ReactiveEffect(getter, () => {
// 调度器:标记为脏数据,触发依赖更新(用 Promise.resolve 异步更新)
trigger(computed, TriggerOpTypes.SET, 'value')
})
const computed = {
get value() {
track(computed, TrackOpTypes.GET, 'value') // 收集计算属性自身的依赖
return _effect.run() // 执行 getter,收集内部依赖
}
} as ComputedRef<T>
return computed
}
3. watch()
与 watchEffect()
(apiWatch.ts)
// watch 的核心实现简写
function doWatch(
source: WatchSource,
cb: WatchCallback,
): StopHandle {
const getter = () => source() // 包装为 getter 函数
const scheduler = () => {
const newValue = effect.run() // 重新计算值
cb(newValue, oldValue) // 触发回调
}
const effect = new ReactiveEffect(getter, scheduler)
effect.run() // 首次运行收集依赖
return () => effect.stop()
}
// watchEffect 是简化版 watch
function watchEffect(effectFn: () => void) {
return doWatch(effectFn, null)
}
4. 模板渲染的 effect(renderer.ts)
// 组件实例初始化时创建渲染 effect
function setupRenderEffect(
instance: ComponentInternalInstance,
) {
const componentUpdateFn = () => {
// 生成子树 vnode(会访问响应式数据)
const subTree = render.call(instance.proxy)
patch(instance.subTree, subTree) // 对比更新
instance.subTree = subTree
}
// 创建渲染 effect(关键!)
instance.update = new ReactiveEffect(componentUpdateFn, () => {
queueJob(instance.update!) // 调度器:将更新加入异步队列
})
// 首次执行收集依赖
instance.update.run()
}
四、依赖关系卸载机制
当组件卸载或手动停止 effect 时调用 ReactiveEffect.stop()
:
class ReactiveEffect {
stop() {
for (const dep of this.deps) {
dep.delete(this) // 从所有依赖的 dep 集合中删除自身
}
this.deps.length = 0 // 清空 deps 数组
}
}
五、验证案例:通过 Effect 源码输出依赖
调试代码示例:
import { reactive, effect, watch } from 'vue'
const state = reactive({ count: 0, msg: 'hello' })
window.__VUE__ = { targetMap } // 暴露到全局
effect(() => { console.log('effect:', state.count) })
watch(() => state.msg, (val) => { console.log('watch:', val) })
在浏览器控制台中输入以下命令观察结构:
// 得到 state 对应的 depsMap
const depsMap = __VUE__.targetMap.get(state)
// 查看 'count' 属性收集的 effect
console.log(depsMap.get('count')) // 输出的 Set 中包含 ReactiveEffect 实例
// 检查 effect 的 fn 属性
Array.from(depsMap.get('count')).map(e => e.fn.toString())
// 可看到具体副作用函数代码
六、重点设计启示
-
统一依赖收集入口
所有副作用最终通过activeEffect
暂存当前上下文,在track()
阶段集中处理,实现了 API 的统一管理。 -
层级化 effect 栈
effectStack
实现嵌套 effect 的正确上下文切换(常见于组件树渲染)。 -
调度器分离
ReactiveEffect.scheduler
允许响应式更新与副作用的实际执行解耦,例如:- 组件渲染使用队列进行批处理更新
watch
的flush: 'post'
选项延迟回调到 DOM 更新后
-
惰性计算优化
computed 属性通过dirty
标志和响应式值的缓存,避免重复计算。
通过这样的源码设计,Vue3 在不同场景下均能高效维护响应式依赖关系,而 targetMap
如同中枢神经将各部分有机连接。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· 单线程的Redis速度为什么快?
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码