冀云
89.99M · 2026-03-05
在 Vue 2 中之所以能“侥幸”运行,是因为 Vue 2 的响应式机制(Object.defineProperty)和 $ refs 的存储机制是隔离的。
但在 Vue 3 中,这一“潜规则”被彻底打破。这不仅仅是命名习惯的问题,而是触及了 Vue 3 响应式原理和 组件实例结构的根本性变革
当 ref="xxx" 和 v-model="xxx"(或 :model="xxx")同名时,在 Vue 3 中通常会出现以下三种情况:
现象:页面初始化正常,但一旦输入框输入内容,或者组件更新,页面报错 TypeError: xxx is not a function 或 Cannot set property xx of undefined。
原因:
setup 返回的对象是响应式的(基于 Proxy)。ref)赋值给这个响应式对象的 xxx 属性。{ name: '' })被直接替换为组件实例(或 DOM 元素)。v-model 尝试更新数据时,发现 xxx 已经变成了一个 DOM 对象,导致赋值失败或类型错误。Proxy 的拦截机制,当你把一个 DOM 元素赋值给原本是数据的变量时,破坏了原有的响应式依赖链条。Vue 无法再追踪到数据的变化。defineExpose 冲突xxxRef.value 调用子组件方法。v-model 和 ref,子组件的实例暴露逻辑会混乱,导致父组件拿到的 ref 是一个数据对象而不是组件实例。除了你提到的 ref 与 v-model 同名问题,升级过程中以下几点也极易出错:
this 指向的变化 (setup 中的坑)Vue 2:data, methods, computed 都挂载在 this 上,随处可用。
Vue 3:setup 函数中没有 this。如果你在 setup 中定义了变量或函数,必须通过 return 暴露给模板,或者使用 ref/reactive 包装。
setup 中直接写函数,模板里调用会报 is not defined。EventBus) 消失Vue 2:常用 new Vue() 实例作为事件总线。
Vue 3:实例不再实现事件接口。 $ on, $ off, $ emit (父传子除外) 被移除。
mitt 或 tiny-emitter)。filters 被移除Vue 2:常用 filters 格式化文本。
Vue 3:filters 被彻底移除。
scoped 样式穿透写法变更>>> 或 /deep/ 穿透 scoped 样式。:deep()。/* Vue 3 写法 */
2.parent :deep(.child) { 3 color: red; 4}
v-if 优先级高于 v-forVue 2:v-for 优先级高于 v-if(虽然不推荐混用)。
Vue 3:v-if 优先级高于 v-for。如果两者同时存在,v-if 将无法访问 v-for 里的变量。
v-if 和 v-for,改用计算属性过滤数据。slot) 语法废弃Vue 2:使用 slot 和 slot-scope attribute。
Vue 3:废弃旧语法,统一使用 v-slot 指令。
#slotName 或 v-slot:slotName。Vue.component, Vue.directive 是全局挂载。createApp 实例上的方法。
// Vue 3 2const app = createApp({}) 3app.component('my-comp', MyComp)
针对 ref 和 v-model 同名的问题,唯一的最佳实践就是“物理隔离”命名:
userForm, searchParams。userFormRef, searchParamsRef。这样不仅解决了 Vue 3 的底层冲突,也让代码的意图一目了然:带 Ref 的是用来操作 DOM 或调用方法的,不带的是用来存数据的