先记住这个答案
flush sync 让侦听器在响应式变化触发时同步运行,watchSyncEffect 则是采用这种调度的 watchEffect。它不会像默认监听那样把一段同步代码里的多次变化合并,因此可能反复执行,并读到多个字段尚未全部更新的中间组合。适合轻量、低频且确实要求立即响应的场景,例如失效一个外部缓存;不适合用来默认处理批量数组更新或等待新 DOM。
- 同步侦听不做默认的批量合并
- 连续字段写入可能暴露中间组合
- 及时失效不等于应该同步做昂贵重算
执行次数与赋值过程直接相关
将数组连续推入许多项时,同步监听可能对每次触发都运行。如果每次回调又扫描整个列表,累计工作会迅速扩大;改为默认批量调度通常能先避免重复处理同一轮的中间状态。
同步只表示回调更早进入执行,并不让组件先完成渲染。把它用于读取新节点会方向相反:你更可能看见旧 DOM,而不是获得“完全同步的界面”。节点需求应选择更新后的时机。
用两个字段观察半更新组合
下例把起点和终点从零、一改为十、二十。同步监听会先看到十、一,再看到十、二十;默认监听经过批量刷新只记录最终组合,这能解释表单联动里短暂触发错误校验的原因。
示例没有在回调里修复区间,因此不会形成额外反馈。真实校验若立刻回写另一个字段,会让操作结果更难推导,应先明确多字段更新边界,再决定何时校验。
import { ref, watch, nextTick } from 'vue'
const start = ref(0)
const end = ref(1)
const syncSeen: string[] = []
const batchedSeen: string[] = []
const stopSync = watch([start, end], ([a, b]) => {
syncSeen.push(a + ':' + b)
}, { flush: 'sync' })
const stopDefault = watch([start, end], ([a, b]) => {
batchedSeen.push(a + ':' + b)
})
start.value = 10
end.value = 20
await nextTick()
stopSync()
stopDefault()syncSeen 包含十比一和十比二十两条记录,batchedSeen 只包含最终十比二十。差别来自调度合并,而不是哪一份 ref 更早完成赋值,也不能据此推断同步方式性能更好。
只让必须立即发生的动作同步
有时下一行代码就要读取外部缓存,此时同步把缓存标记为失效可以成立。但失效标记应尽量轻量,真正重建大结果可以按需或稍后执行,避免把每次输入都变成昂贵的同步阻塞。
明确记录使用 sync 的理由,例如不允许同一调用栈里读取过期缓存,并测试连续多字段修改。若只是觉得回调晚了一点或日志顺序不直观,应先理解默认调度,不要直接改变所有观察者的执行模式。
容易答错的地方
- 认为同步一定更快更可靠
- 更早执行不代表更少工作,多次触发可能增加计算量,还可能把中间状态提交给外部系统。应围绕是否必须立即响应评估,而不是以日志出现得早来判断整体性能。
- 给深度数组监听一律加 sync
- 批量插入和嵌套变化可能带来高频同步回调,如果回调还遍历整份数据,开销会叠加。优先缩小监听源、合并操作或观察最终派生结果,再为确有必要的低成本动作考虑同步模式。
面试官还会怎么问?
watchSyncEffect 与 watch 加 sync 完全相同吗?
执行时机相近,但依赖模型仍不同。watchSyncEffect 自动收集同步读取并初次运行,watch 仍依赖显式源且默认不立即执行回调;调度别名不会改变它们各自的接口职责。
如何避免多字段同步观察到不合法中间值?
可以在一次更新中整体替换包含相关字段的对象,或者让业务校验在明确的提交边界执行。也可使用默认批量观察最终组合,但要根据是否允许异步刷新来决定,不能仅靠条件补丁掩盖数据关系。
同步回调中修改自身源会更危险吗?
会让反馈更直接进入当前执行链,可能快速递归或产生难以追踪的嵌套调用。根本修复仍是避免无界写回或建立稳定收敛条件,调度方式本身不负责让错误的数据循环终止。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。