问题场景
团队把一个 Vue2 中大型后台管理系统升级到 Vue3,提测当天,发现20+ 个表单弹窗的输入值绑定位数错乱 ------比如价格输入 100,回显变成 1.00;日期选择器选了 2026-06-11,父组件拿到的却是 [object Object]。
查了一圈,所有 input 组件都用了 :value.sync="xxx" 写法。
原因分析
Vue2 的 .sync 修饰符本质是语法糖:
vue
<!-- Vue2: :title.sync="foo" 等价于 -->
<Child :title="foo" @update:title="foo = $event" />
但在 Vue3 中,.sync 被完全移除 ,全家桶统一改用 v-model 支持多个绑定:
vue
<!-- Vue3 写法 -->
<Child v-model:title="foo" />
升级工具 vue-demi / gogocode 不会自动转换 .sync 到 v-model:xxx,它只是把编译通过的代码保留原样。最终结果是:
<Child :value.sync="price" />→ Vue3 运行时忽略.sync,value 变成单向绑定- 子组件
$emit('update:value', newVal)→ 父组件监听不到 - 表单数据交互直接断掉
解决方案
全局替换脚本(用 sed 或 AST 工具批量处理):
方案 A:手动正则替换(小项目)
bash
# 匹配 :xxx.sync 替换为 v-model:xxx
find src -name "*.vue" -exec sed -i '' \
's/:([a-zA-Z]*).sync/v-model:\1/g' {} +
方案 B:用 @vue/compiler-sfc AST 精准替换(推荐)
安装 vue3-migration-tool 社区包:
bash
npx vue3-sync-codemod src/**/*.vue
它会自动处理:
:visible.sync="dialogShow"→v-model:visible="dialogShow"@click.native等修饰符同步迁移
方案 C:兼容垫片(紧急修复)
在全局注册一个 Mixin / Directive 做运行时兼容:
typescript
// sync-shim.ts --- 仅作紧急兜底,不推荐长期使用
app.mixin({
created() {
const props = this.$options.props || {}
Object.keys(props).forEach(key => {
const syncEvent = `update:${key}`
// 如果父组件写了 .sync,Vue3 会把 update:xxx 事件绑定到 $listeners
// 手动桥接
})
}
})
要点总结
| 对比项 | Vue2 | Vue3 |
|---|---|---|
| 单 v-model | v-model + :value |
v-model(默认 modelValue) |
| 多绑定 | :a.sync :b.sync |
v-model:a v-model:b |
| .native 修饰符 | @click.native |
默认就是 native,无需修饰符 |
| $listeners | 独立对象 | 合并到 $attrs |
升级前在项目里全局搜 .sync 和 .native,这两个是 Vue3 最高频升级坑。 拿 grep 跑一遍再提测,能省至少半天的回归测试时间。