Skip to content

在 Vue 3 的 watch API 中,flush 选项用于控制侦听器回调的执行时机。这是一个非常精细的性能优化工具,理解 prepostsync 三种模式,能让你的代码在正确的时间点运行,避免潜在的竞态条件或 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']
// 每次赋值都立即触发回调

对比 presync

如果使用 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 的关系

watchEffectflush 选项行为类似,但默认值不同:

  • 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 不等更新直接跑。