Vue3 的响应式系统源码级 watch 的 immediate 与 deep 选项实现原理
字数 1908 2025-12-07 22:36:09
Vue3 的响应式系统源码级 watch 的 immediate 与 deep 选项实现原理
1. 题目描述
本题聚焦于 Vue3 的 watch API 中 immediate 和 deep 两个核心选项的源码级实现原理,包括:
immediate: true如何让回调立即执行deep: true如何深度监听对象/数组的变化- 二者在响应式系统中的协同工作流程
2. watch 的初始化与 immediate 实现原理
2.1 watch 的入口
在 Vue3 中,watch 函数(packages/runtime-core/src/apiWatch.ts)首先会标准化参数,然后创建 ReactiveEffect 实例。
核心步骤:
function watch(source, cb, options) {
const { immediate, deep, flush } = options
// 1. 标准化 source(支持 getter/ref/reactive/数组)
const getter = isReactive(source)
? () => source
: isRef(source)
? () => source.value
: isFunction(source)
? source
: () => traverse(source) // deep 处理
// 2. 如果 deep=true,包装 getter
if (deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
// 3. 定义 job 任务
const job = () => {
if (!effect.active) return
if (cb) {
const newValue = effect.run()
// 触发回调...
}
}
// 4. 创建 effect
const effect = new ReactiveEffect(getter, scheduler)
// 5. immediate 处理
if (immediate) {
job() // 立即执行一次
} else {
// 初始执行但不触发回调
oldValue = effect.run()
}
}
2.2 immediate 的执行时机
- 当
immediate: true时,job()会在watch创建后同步执行 - 此时
effect.run()会:- 执行
getter收集依赖 - 返回当前值作为
oldValue - 触发回调函数
cb(newValue, oldValue, onCleanup)
- 执行
注意:首次执行的 oldValue 是 undefined,因为还没有上一次的值。
3. deep 选项的深度监听原理
3.1 traverse 深度遍历函数
关键函数 traverse(位于 packages/reactivity/src/baseHandlers.ts 或相关文件):
function traverse(value, seen = new Set()) {
// 1. 基础类型或已访问过则直接返回
if (!isObject(value) || seen.has(value)) {
return value
}
seen.add(value)
// 2. 如果是数组,遍历每个元素
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen)
}
}
// 3. 如果是对象,递归每个属性
else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], seen)
}
}
// 4. 处理 Map/Set 等集合类型
else if (value instanceof Map || value instanceof Set) {
value.forEach((v) => {
traverse(v, seen)
})
}
return value
}
3.2 deep 的依赖收集机制
当 deep: true 时,watch 会将 getter 包装:
// 简化版本
if (deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter() // 获取原始值
traverse(val) // 深度访问所有属性
return val
}
}
这个过程会:
- 先获取监听的目标值
- 通过
traverse递归访问对象的所有嵌套属性 - 每个被访问的属性都会触发其
get拦截器,收集当前watch的effect作为依赖 - 之后任意嵌套属性变化,都会触发这个
effect重新执行
4. immediate 与 deep 的协同工作
4.1 执行顺序
- 创建阶段:
// 伪代码 watch( obj, () => { console.log('changed') }, { immediate: true, deep: true } ) - 先处理
deep:将getter包装为可深度遍历的版本 - 再处理
immediate:立即执行job() job()中执行effect.run()→ 执行包装后的getter→ 触发traverse→ 收集所有嵌套属性的依赖- 触发回调函数
4.2 内存管理
- deep 的代价:
traverse会访问所有属性,如果对象很大,首次执行和依赖收集成本高 - Set 防循环引用:
seen集合防止对象循环引用导致的无限递归 - 清理机制:
watch返回的停止函数会调用effect.stop(),清除所有收集的依赖
5. 源码中的关键细节
5.1 避免重复收集
在 traverse 中:
if (seen.has(value)) {
return value // 已访问过的对象不再遍历
}
seen.add(value)
这里用 Set 存储已访问对象,解决:
- 循环引用(a.b = a)
- 重复引用(a.b = obj, a.c = obj)
5.2 与 reactive 系统的集成
深度监听依赖 reactive 系统的 track 函数:
- 每个属性的
get操作会调用track(target, TrackOpTypes.GET, key) track将当前effect存入该属性的depsMap[key]中- 深度遍历确保了所有被访问属性都收集了同一个
watch effect
5.3 immediate 的 scheduler 调度
当 immediate: true 且设置了 flush: 'post' 时:
if (immediate) {
if (flush === 'sync') {
job() // 同步执行
} else if (flush === 'post') {
queuePostRenderEffect(job, instance && instance.suspense)
} else {
job() // 默认 'pre' 也立即执行
}
}
6. 总结实现原理
| 选项 | 核心原理 | 关键函数 |
|---|---|---|
| immediate | 在 watch 初始化后同步/异步执行 job(),触发回调 |
job() 直接调用 |
| deep | 通过 traverse 递归访问对象所有属性,强制收集深层依赖 |
traverse() 深度遍历 |
协同工作流程:
- 参数标准化 → 2. deep 包装 getter → 3. 创建 effect → 4. immediate 触发首次执行 → 5. traverse 深度收集依赖 → 6. 回调执行
性能影响:
immediate增加一次初始回调执行deep增加首次遍历开销和更广的依赖收集范围,需谨慎使用大型对象