vue3 watch getter执行时机

在 Vue 3 的响应式系统中,watch 的 getter 函数执行时机与触发机制是高度优化后的设计。以下从源码层级深度解析其运作原理(基于 Vue 3.4.27 版本),通过流程图和关键源码片段说明整个过程。


一、执行时机的两个阶段

1. 初始化阶段

源码入口: packages/runtime-core/src/apiWatch.tsdoWatch 函数
关键流程:

// doWatch 局部代码
const effect = new ReactiveEffect(getter, scheduler)
const oldValue = effect.run() // 🔥首次执行 getter

具体表现:
watch 被创建时,立即执行一次 getter 函数用于:

  • 收集初始依赖(通过响应式系统的 track
  • 记录初始值(作为后续回调的 oldValue

响应式触发机制:
若 getter 中访问了 ref.valuereactive 对象的属性,会触发 track,建立依赖关系:

// 响应式核心实现(简化)
function track(target, type, key) {
  if (activeEffect) { // 当前活动的 effect(即 watch 的 effect)
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect) // 🔥将 effect 添加到依赖中
  }
}

2. 更新触发阶段

触发条件:
当被侦听的响应式数据发生变更时(如修改 ref.valuereactive 对象的属性)。

响应式系统触发流程:

sequenceDiagram participant User Code participant Proxy set participant trigger 函数 participant scheduler User Code ->> Proxy set: 修改数据(如obj.a = 2) Proxy set ->> trigger 函数: 通知属性变更 trigger 函数 ->> scheduler: 找到相关 effect 的调度器 scheduler ->> Queue: 根据 flush 模式将任务入队 Queue ->> Effect: 异步/同步执行 effect 的 run()

关键源码:

// packages/reactivity/src/effect.ts 的 trigger 函数
export function trigger(target, type, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const effects = new Set<ReactiveEffect>()
  // 收集所有相关 effects
  depsMap.get(key)?.forEach(effect => {
    effects.add(effect)
  })

  // 🔥执行调度
  effects.forEach(effect => {
    if (effect.scheduler) {
      effect.scheduler() // 调用 watch 的 scheduler
    } else {
      effect.run() // 默认运行
    }
  })
}

实际效果:
当数据变化时,通过调度器 (scheduler) 控制 getter 的二次执行。


二、调度控制与 Getter 二次执行

1. 调度器 (scheduler) 的作用

doWatch 中创建的 scheduler 控制着 getter 何时执行:

scheduler = () => {
  // 🔥根据 flush 选项决定执行方式
  if (!instance || instance.isMounted) {
    queueJobWithCleanup(job, /* ... */)
  }
}

不同 flush 模式的行为

模式 执行队列 源码逻辑
pre 组件更新前 加入 queuePreFlushCb,在组件更新前置队列执行
post 组件更新后 加入 queuePostFlushCb(默认行为)
sync 同步执行 直接调用 job,保留旧同步逻辑

源码片段:

// queueJobWithCleanup 的 flush 处理(简化)
if (flush === 'sync') {
  job()
} else if (flush === 'pre') {
  queuePreFlushCb(job)
} else {
  queuePostFlushCb(job)
}

2. Getter 的二次执行

任务 (job) 的具体内容

const job = () => {
  // 只有当存在回调时才处理
  if (cb) {
    const newValue = effect.run() // 🔥再次执行 getter 获取新值
    if (deep || hasChanged(newValue, oldValue)) {
      callAsyncCb(cb, [newValue, oldValue, onInvalidate])
      oldValue = newValue // 更新旧值
    }
  }
}

细节解释:

  • effect.run():重新执行 getter,此时因响应式数据变化,可能收集到新的依赖
  • 新旧值对比:通过 hasChanged 避免不必要的回调触发
  • 依赖更新:如果 getter 内的依赖关系变化(如条件分支不同),会动态更新依赖集合

三、性能优化设计

1. 旧值存储与优化

Vue 使用闭包存储 oldValue

let oldValue: any = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  • 首次执行后存储旧值,避免重复计算
  • 仅当依赖数据变更时才重新读取新值

2. 深度侦听优化

对于 reactive 对象,启用 deep 后会递归遍历属性:

getter = () => traverse(source)

仅在初始化时深度遍历一次,后续变更通过 Proxy 精确触发。


四、示例结合源码的全流程

场景示例

const count = ref(0)
watch(count, (newVal) => console.log(newVal))

全流程分解

  1. 初始化阶段

    • 创建 effect,getter = () => count.value
    • 首次执行 getter:触发 counttrackcounteffect 建立关联
    • 存储 oldValue = 0
  2. 数据变更

    count.value++ // 触发 set 陷阱
    
    • trigger 被调用,发现关联的 effect
    • 调度 effect 的 scheduler(对于单个 ref 默认 flush: 'pre'
  3. 调度阶段

    • job 加入 queuePreFlushCb
    • 在组件更新前执行微任务队列
  4. Job 执行

    • effect.run() 再次执行 getter 得到 newValue = 1
    • 触发回调 console.log(1)
    • 更新 oldValue = 1

五、核心流程图解

graph TD A[创建 watch] --> B[执行 doWatch] B --> C[创建 effect 并立即运行 getter] C --> D[建立响应式依赖 track] D --> E[响应式数据变更] E --> F[触发 trigger] F -->|调度器介入| G{判断 flush 模式} G -->|pre| H[加入队列 queuePreFlushCb] G -->|post| I[加入队列 queuePostFlushCb] G -->|sync| J[立即执行 job] H & I & J --> K[job 执行] K -->|执行 effect.run| L[重新运行 getter 取新值] L --> M{值是否变化?} M -->|是| N[调用回调] M -->|否| O[跳过回调]

六、关键总结

  1. 执行时机

    • 初始化立即执行一次,用于收集依赖
    • 响应式数据变更后按调度策略(pre/post/sync)异步/同步执行
  2. 触发机制本质
    通过响应式系统的 track/trigger 实现精准依赖追踪

  3. 性能关键点

    • 安全的依赖清理(通过 effect.stop 停止时清理)
    • 新旧值对比跳过无效回调
    • 分层队列管理避免重复执行

理解 Vue 中 getter 的执行机制,有助于写出更高效的侦听逻辑,并能在需要时通过 flush 选项精确控制副作用执行顺序。

posted @ 2025-02-09 14:36  木燃不歇  阅读(134)  评论(0)    收藏  举报