在 Vue 3 的 watch API 中,flush 选项用于控制侦听器回调的执行时机。这是一个非常精细的性能优化工具,理解 pre、post 和 sync 三种模式,能让你的代码在正确的时间点运行,避免潜在的竞态条件或 DOM 操作错误。
一、flush 选项的三种模式
| 模式 | 执行时机 | 适用场景 |
|---|---|---|
pre | 在组件更新之前,同步执行 | 需要在 DOM 更新前读取旧状态 |
post | 在组件更新之后,异步执行 | 需要访问更新后的 DOM 或组件实例 |
sync | 同步执行,不缓冲 | 需要立即响应,不依赖组件更新周期 |
二、1. flush: 'pre' —— 组件更新前执行
这是 watch 的默认模式。
执行时机
- 当侦听的源发生变化时,
watch回调会在当前组件的render函数执行之前同步运行。 - 它发生在 Vue 的更新队列(
queueJob)中,优先级高于渲染。
代码示例
js
import { ref, watch, onMounted } from 'vue';
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`Pre flush: count is ${newVal}`);
// 此时 DOM 尚未更新
}, { flush: 'pre' }); // 默认,可省略
// 模拟更新
count.value = 1;
// 输出: "Pre flush: count is 1"
// 然后组件重新渲染,DOM 更新适用场景
- 读取更新前的状态:比如记录某个值变化前的快照。
- 性能敏感的计算:需要在渲染前准备好数据,避免在
render中进行昂贵计算。 - 与
computed类似:computed本质上也是在pre阶段求值。
注意事项
- 回调执行时,组件的 DOM 还是旧的。
- 如果你在回调中访问 DOM,看到的是变化前的状态。
三、2. flush: 'post' —— 组件更新后执行
执行时机
- 当侦听的源发生变化时,
watch回调会被推入一个微任务队列,在组件完成渲染并更新 DOM 之后执行。 - 它使用
Promise.then()或queuePostFlushCb实现异步延迟。
代码示例
js
import { ref, watch, onMounted } from 'vue';
const count = ref(0);
const el = ref(null);
onMounted(() => {
console.log(el.value.textContent); // "0"
});
watch(count, (newVal, oldVal) => {
// 此时 DOM 已经更新
console.log(`Post flush: DOM text is ${el.value.textContent}`); // "1"
console.log(`Post flush: count is ${newVal}`); // "1"
}, { flush: 'post' });
count.value = 1;
// 组件先重新渲染,DOM 更新为 "1"
// 然后执行 watch 回调适用场景
- 操作更新后的 DOM:如测量元素尺寸、触发动画、聚焦输入框。
- 依赖组件实例的方法或属性:确保组件已完全更新。
- 避免 DOM 读写颠簸:将 DOM 读取和写入分开,提升性能。
为什么需要 post?
Vue 的更新是异步批处理的。如果你在 count.value 改变后立即访问 DOM,可能 DOM 还没更新。post 确保你在安全的时机操作 DOM。
四、3. flush: 'sync' —— 同步立即执行
执行时机
- 当侦听的源发生变化时,
watch回调立即同步执行,不经过 Vue 的更新队列。 - 它类似于
watchEffect的默认行为,但更强制。
代码示例
js
import { ref, watch } from 'vue';
const count = ref(0);
let syncLog = [];
watch(count, (newVal, oldVal) => {
syncLog.push(`Sync: ${newVal}`);
}, { flush: 'sync' });
// 模拟一系列更新
count.value = 1;
count.value = 2;
count.value = 3;
console.log(syncLog);
// 输出: ['Sync: 1', 'Sync: 2', 'Sync: 3']
// 每次赋值都立即触发回调对比 pre 和 sync
如果使用 flush: 'pre':
js
// 使用 pre
count.value = 1;
count.value = 2;
count.value = 3;
// 由于 Vue 的批处理,watch 只会在下一次事件循环前执行一次
// 回调接收的 newVal 是 3,oldVal 是 0
// 只记录一次:'Pre: 3'适用场景
- 需要立即响应:如状态同步到外部系统(Redux、Pinia)、日志记录。
- 不依赖 Vue 渲染周期:你的逻辑与组件更新无关。
- 精确追踪每次变化:即使在一次事件循环中多次修改,也希望每次都被捕获。
性能警告
- 性能开销大:每次变化都执行,可能造成性能瓶颈。
- 破坏批处理:绕过 Vue 的异步更新优化,可能导致不必要的重复计算。
- 谨慎使用:仅在必要时使用。
五、flush 与 watchEffect 的关系
watchEffect 的 flush 选项行为类似,但默认值不同:
watchEffect默认flush: 'pre'。- 你可以设置
flush: 'post'或'sync'。
js
watchEffect(() => {
// ...
}, { flush: 'post' });六、如何选择 flush 模式?
| 你的需求 | 推荐 flush 模式 |
|---|---|
| 需要在渲染前准备好数据(如缓存计算) | 'pre' (默认) |
| 需要操作更新后的 DOM(如测量、动画) | 'post' |
| 需要立即同步执行,不缓冲 | 'sync' |
| 记录日志或同步到外部状态 | 'sync' |
| 避免在渲染过程中进行昂贵操作 | 'post' |
七、总结
flush 选项是 watch 的“时间控制器”:
pre:更新前,同步执行。默认模式,适合预处理。post:更新后,异步执行。适合 DOM 操作。sync:立即,同步执行。适合外部同步,慎用。
理解这三种模式,你就能精确控制副作用的执行时机,写出更健壮、更高效的 Vue 应用。记住:
pre看旧 DOM,post看新 DOM,sync不等更新直接跑。