vue3 watch getter执行时机
在 Vue 3 的响应式系统中,watch 的 getter 函数执行时机与触发机制是高度优化后的设计。以下从源码层级深度解析其运作原理(基于 Vue 3.4.27 版本),通过流程图和关键源码片段说明整个过程。
一、执行时机的两个阶段
1. 初始化阶段
源码入口: packages/runtime-core/src/apiWatch.ts 的 doWatch 函数
关键流程:
// doWatch 局部代码
const effect = new ReactiveEffect(getter, scheduler)
const oldValue = effect.run() // 🔥首次执行 getter
具体表现:
当 watch 被创建时,立即执行一次 getter 函数用于:
- 收集初始依赖(通过响应式系统的
track) - 记录初始值(作为后续回调的
oldValue)
响应式触发机制:
若 getter 中访问了 ref.value 或 reactive 对象的属性,会触发 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.value 或 reactive 对象的属性)。
响应式系统触发流程:
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))
全流程分解
-
初始化阶段:
- 创建 effect,
getter = () => count.value - 首次执行 getter:触发
count的track,count→effect建立关联 - 存储
oldValue = 0
- 创建 effect,
-
数据变更:
count.value++ // 触发 set 陷阱trigger被调用,发现关联的 effect- 调度 effect 的
scheduler(对于单个ref默认flush: 'pre')
-
调度阶段:
- 将
job加入queuePreFlushCb - 在组件更新前执行微任务队列
- 将
-
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[跳过回调]
六、关键总结
-
执行时机:
- 初始化立即执行一次,用于收集依赖
- 响应式数据变更后按调度策略(
pre/post/sync)异步/同步执行
-
触发机制本质:
通过响应式系统的track/trigger实现精准依赖追踪 -
性能关键点:
- 安全的依赖清理(通过
effect.stop停止时清理) - 新旧值对比跳过无效回调
- 分层队列管理避免重复执行
- 安全的依赖清理(通过
理解 Vue 中 getter 的执行机制,有助于写出更高效的侦听逻辑,并能在需要时通过 flush 选项精确控制副作用执行顺序。

浙公网安备 33010602011771号