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 对象的属性)。

响应式系统触发流程:

EffectQueueschedulertrigger 函数Proxy setUser CodeEffectQueueschedulertrigger 函数Proxy setUser Code修改数据(如obj.a = 2)通知属性变更找到相关 effect 的调度器根据 flush 模式将任务入队异步/同步执行 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

五、核心流程图解

调度器介入

pre

post

sync

执行 effect.run

创建 watch

执行 doWatch

创建 effect 并立即运行 getter

建立响应式依赖 track

响应式数据变更

触发 trigger

判断 flush 模式

加入队列 queuePreFlushCb

加入队列 queuePostFlushCb

立即执行 job

job 执行

重新运行 getter 取新值

值是否变化?

调用回调

跳过回调


六、关键总结

  1. 执行时机

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

  3. 性能关键点

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

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

posted @   木燃不歇  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示