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
对象的属性)。
响应式系统触发流程:
关键源码:
// 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
五、核心流程图解
六、关键总结
-
执行时机:
- 初始化立即执行一次,用于收集依赖
- 响应式数据变更后按调度策略(
pre/post/sync
)异步/同步执行
-
触发机制本质:
通过响应式系统的track/trigger
实现精准依赖追踪 -
性能关键点:
- 安全的依赖清理(通过
effect.stop
停止时清理) - 新旧值对比跳过无效回调
- 分层队列管理避免重复执行
- 安全的依赖清理(通过
理解 Vue 中 getter 的执行机制,有助于写出更高效的侦听逻辑,并能在需要时通过 flush
选项精确控制副作用执行顺序。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY