Vue3 的响应式系统源码级 watch 的 immediate 与 deep 选项实现原理
字数 1908 2025-12-07 22:36:09

Vue3 的响应式系统源码级 watch 的 immediate 与 deep 选项实现原理

1. 题目描述

本题聚焦于 Vue3 的 watch API 中 immediatedeep 两个核心选项的源码级实现原理,包括:

  • 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() 会:
    1. 执行 getter 收集依赖
    2. 返回当前值作为 oldValue
    3. 触发回调函数 cb(newValue, oldValue, onCleanup)

注意:首次执行的 oldValueundefined,因为还没有上一次的值。


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
  }
}

这个过程会:

  1. 先获取监听的目标值
  2. 通过 traverse 递归访问对象的所有嵌套属性
  3. 每个被访问的属性都会触发其 get 拦截器,收集当前 watcheffect 作为依赖
  4. 之后任意嵌套属性变化,都会触发这个 effect 重新执行

4. immediate 与 deep 的协同工作

4.1 执行顺序

  1. 创建阶段
    // 伪代码
    watch(
      obj, 
      () => { console.log('changed') },
      { immediate: true, deep: true }
    )
    
  2. 先处理 deep:将 getter 包装为可深度遍历的版本
  3. 再处理 immediate:立即执行 job()
  4. job() 中执行 effect.run() → 执行包装后的 getter → 触发 traverse → 收集所有嵌套属性的依赖
  5. 触发回调函数

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() 深度遍历

协同工作流程

  1. 参数标准化 → 2. deep 包装 getter → 3. 创建 effect → 4. immediate 触发首次执行 → 5. traverse 深度收集依赖 → 6. 回调执行

性能影响

  • immediate 增加一次初始回调执行
  • deep 增加首次遍历开销和更广的依赖收集范围,需谨慎使用大型对象
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 实例。 核心步骤: 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 或相关文件): 3.2 deep 的依赖收集机制 当 deep: true 时, watch 会将 getter 包装: 这个过程会: 先获取监听的目标值 通过 traverse 递归访问对象的所有嵌套属性 每个被访问的属性都会触发其 get 拦截器,收集当前 watch 的 effect 作为依赖 之后任意嵌套属性变化,都会触发这个 effect 重新执行 4. immediate 与 deep 的协同工作 4.1 执行顺序 创建阶段 : 先处理 deep :将 getter 包装为可深度遍历的版本 再处理 immediate :立即执行 job() job() 中执行 effect.run() → 执行包装后的 getter → 触发 traverse → 收集所有嵌套属性的依赖 触发回调函数 4.2 内存管理 deep 的代价 : traverse 会访问所有属性,如果对象很大,首次执行和依赖收集成本高 Set 防循环引用 : seen 集合防止对象循环引用导致的无限递归 清理机制 : watch 返回的停止函数会调用 effect.stop() ,清除所有收集的依赖 5. 源码中的关键细节 5.1 避免重复收集 在 traverse 中: 这里用 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' 时: 6. 总结实现原理 | 选项 | 核心原理 | 关键函数 | |------|---------|----------| | immediate | 在 watch 初始化后同步/异步执行 job() ,触发回调 | job() 直接调用 | | deep | 通过 traverse 递归访问对象所有属性,强制收集深层依赖 | traverse() 深度遍历 | 协同工作流程 : 参数标准化 → 2. deep 包装 getter → 3. 创建 effect → 4. immediate 触发首次执行 → 5. traverse 深度收集依赖 → 6. 回调执行 性能影响 : immediate 增加一次初始回调执行 deep 增加首次遍历开销和更广的依赖收集范围,需谨慎使用大型对象