Vue —— Vue 3 草稿回填踩坑实录:watch 异步执行引发的竞态条件与解决方案

张开发
2026/4/16 3:07:59 15 分钟阅读

分享文章

Vue —— Vue 3 草稿回填踩坑实录:watch 异步执行引发的竞态条件与解决方案
Vue表单草稿回填的时序问题与解决方案问题背景在业务开发中草稿功能是常见需求用户填写表单时可以保存草稿下次继续编辑时自动回填之前的数据。最近在开发表单功能时遇到一个棘手的问题草稿回填时子表数据被清空。具体场景用户在表单中填写了明细列表并保存草稿再次打开草稿继续编辑时明细列表却是空的其他字段如类型、金额等回填正常问题分析代码结构表单使用Vue 3 Composition API简化后的代码结构如下vuescript setup const formData reactive({ category: , // 表单分类类型A/类型B/类型C items: [] // 明细列表 }) // 监听分类变化重置明细模板 watch(() formData.category, (newCategory) { // 根据分类设置不同的明细模板 if (newCategory typeA) { formData.items [{ name: , quantity: 0, price: 0 }] } else if (newCategory typeB) { formData.items [{ title: , amount: 0, remark: }] } else { formData.items [{ description: , value: 0 }] } }) // 加载草稿数据 const loadDraftData async (draftId) { const res await api.getDraft(draftId) // 回填表单数据 formData.category res.data.category // 触发watcher formData.items res.data.items // 被watcher覆盖 } /script问题根因问题出在数据回填的时序上loadDraftData设置formData.category res.data.category这会触发watch执行明细重置逻辑watcher 将formData.items重置为空白模板接着执行formData.items res.data.items但由于 Vue 的响应式更新是异步批量处理的watcher 的重置可能在赋值之后执行最终结果明细数据被 watcher 覆盖为空白模板这是一个典型的watcher 与数据加载的竞态问题。时序图textloadDraftData() | v 设置 category ────── 触发 watcher异步队列 | | v v 设置 items watcher 重置 items | | v v 期望草稿数据 实际空白模板被覆盖解决方案方案一Hydrating 标记推荐引入一个标记变量在数据回填期间跳过 watcher 的重置逻辑vuescript setup const formData reactive({ category: , items: [] }) // 标记是否正在回填草稿数据 const isHydratingDraft ref(false) watch(() formData.category, (newCategory) { // 草稿回填期间跳过重置逻辑 if (isHydratingDraft.value) { return } // 正常的分类切换重置明细模板 if (newCategory typeA) { formData.items [{ name: , quantity: 0, price: 0 }] } else if (newCategory typeB) { formData.items [{ title: , amount: 0, remark: }] } else { formData.items [{ description: , value: 0 }] } }) const loadDraftData async (draftId) { const res await api.getDraft(draftId) // 开启回填模式 isHydratingDraft.value true try { // 回填所有数据 formData.category res.data.category formData.items res.data.items // 等待响应式更新完成 await nextTick() } finally { // 关闭回填模式 isHydratingDraft.value false } } /script优点逻辑清晰易于理解不影响正常的用户交互逻辑可以复用于其他类似场景方案二调整赋值顺序 nextTickvuescript setup const loadDraftData async (draftId) { const res await api.getDraft(draftId) // 先设置 category等待 watcher 执行完毕 formData.category res.data.category await nextTick() // 再设置 items覆盖 watcher 的结果 formData.items res.data.items } /script缺点依赖执行顺序容易被后续修改破坏如果有多个 watcher 相互影响nextTick 可能不够方案三使用 watchEffect 的 flush: ‘sync’vuescript setup watch(() formData.category, (newCategory) { // ... }, { flush: sync }) // 同步执行立即触发 /script缺点同步执行可能影响性能不符合 Vue 推荐的异步更新模式扩展通用的 Hydrating 模式可以封装成一个通用的 composabletypescript// useHydrating.ts import { ref, readonly, nextTick } from vue export function useHydrating() { const isHydrating ref(false) const hydrate async T(fn: () PromiseT): PromiseT { isHydrating.value true try { const result await fn() await nextTick() return result } finally { isHydrating.value false } } const skipIfHydrating (fn: () void) { if (!isHydrating.value) { fn() } } return { isHydrating: readonly(isHydrating), hydrate, skipIfHydrating } }使用方式vuescript setup const { isHydrating, hydrate, skipIfHydrating } useHydrating() watch(() formData.category, () { skipIfHydrating(() { // 重置逻辑 }) }) const loadDraftData async (draftId) { await hydrate(async () { const res await api.getDraft(draftId) Object.assign(formData, res.data) }) } /script总结问题本质Vue 的 watcher 是异步执行的在数据批量回填时可能产生竞态条件推荐方案使用isHydrating标记在数据回填期间跳过 watcher 的副作用逻辑最佳实践区分用户交互触发和程序回填触发两种场景watcher 中应该只处理用户交互场景数据回填使用专门的标记来控制命名建议hydrating水合这个词来自 React/Vue SSR 的概念表示将数据填充到已有结构中非常适合描述草稿回填的场景本文源于实际项目中的问题修复经验希望对遇到类似问题的开发者有所帮助。

更多文章