从 Vue 2 升级到 Vue 3,并不是简单地换一个版本号。底层响应式系统的重写、框架 API 的重新设计以及部分特性的移除,带来了一系列需要开发者主动适配的破坏性变更。好在这些变更大多有规律可循,本节将最常遇到的变更整理成一份速查清单,帮助你快速评估迁移成本。
一、全局 API 的调用方式变了
Vue 2 中通过 Vue.xxx 直接修改全局行为,这在多应用实例场景下会造成污染。Vue 3 引入了 应用实例 的概念,任何全局配置都挂载在 createApp 返回的实例上。
Vue 2 → Vue 3 对照:
// Vue 2
import Vue from 'vue'
Vue.config.ignoredElements = ['my-el']
Vue.component('MyButton', MyButton)
Vue.prototype.$http = axios
new Vue({ render: h => h(App) }).$mount('#app')
// Vue 3
import { createApp } from 'vue'
const app = createApp(App)
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('my-')
app.component('MyButton', MyButton)
app.config.globalProperties.$http = axios // 替代 Vue.prototype
app.mount('#app')
影响面:所有通过 Vue.prototype 挂载的工具方法/属性(如 $http、$bus)需要改为 app.config.globalProperties;Vue.component、Vue.directive 等需通过应用实例调用。
二、响应式系统从 Object.defineProperty 改为 Proxy
这是一项彻底的底层升级,对开发者来说是几乎完全的利好,但也带来了几个细微的行为差异。
解决的历史痛点:
- 对象新增/删除属性自动响应(不再需要
Vue.set/Vue.delete) - 数组索引修改和
length赋值可直接响应 - 深层嵌套对象的初始化性能更好
Vue 3 中的变动点:
reactive对象解构后不再自动保持响应式
const { name } = reactive({ name: 'vue' }) 得到的 name 是一个普通字符串。解决方式是使用 toRefs 或继续通过原对象访问。
ref在模板中自动解包,但在setup的return或reactive中不会
这一点 Vue 3 通过编译器和 proxy 做了大量屏蔽,但在组合式函数中返回 ref 时需要显式 .value。
this不再自动响应式代理
如果你在选项式 API 中将数据放在 data 之外,Vue 2 会将其变为响应式,Vue 3 不会。所有响应式数据必须来自 data、setup 等标准入口。
三、v-model 双向绑定重新设计
Vue 2 中,一个组件只能有一个 v-model(对应 value prop 和 input 事件),如果要绑定多个值,需要用 .sync 修饰符。Vue 3 统一了这两套机制。
变化:
- 默认
v-model对应的 prop 从value改为modelValue,事件从input改为update:modelValue。 - 支持 多个
v-model绑定:v-model:title="title"会使用titleprop 和update:title事件。 .sync修饰符被移除,原来.sync的场景直接用v-model:propName替代。
迁移示例:
<!-- Vue 2:单个 v-model + .sync -->
<MyInput v-model="text" :visible.sync="show" />
<!-- Vue 3:多 v-model -->
<MyInput v-model="text" v-model:visible="show" />
注意:自定义组件的 v-model 用法需要更新 props 与 emits 声明,但大部分 UI 组件库(Element Plus 等)已经做了适配。
四、过滤器(Filters)被彻底移除
Vue 2 中可以在模板中使用 {{ message | capitalize }} 这种管道符语法,Vue 3 不再支持。官方认为过滤器的功能完全可以被计算属性或方法调用所替代,而且更清晰。
迁移方案:将过滤器定义为全局或局部方法,在模板中直接调用:
<!-- Vue 2 过滤器 -->
{{ price | currency }}
<!-- Vue 3 替代方案 -->
{{ currency(price) }}
五、事件总线 API 被移除($on / $off / $once)
Vue 2 允许使用 new Vue() 作为事件总线,实现跨组件通信。Vue 3 的应用实例不再实现事件发射器接口,因此这三个方法不可用。
替代方案:
- 父子通信用
props+emits - 跨层级通信用
provide / inject - 全局状态管理用 Pinia 或 Vuex
- 强依赖事件总线场景可引入第三方库(如
mitt)
六、生命周期钩子更名
Vue 3 对两个生命周期的命名进行了语义化调整,旧名称依旧能用(在选项式 API 中,直到某个版本后才废弃),但推荐尽快迁移。
| Vue 2 选项式 | Vue 3 选项式 | 说明 |
|--------------|--------------|------|
| beforeDestroy | beforeUnmount | 组件销毁前 |
| destroyed | unmounted | 组件销毁后 |
组合式 API 中对应钩子分别为 onBeforeUnmount、onUnmounted。
七、v-if 与 v-for 优先级逆转
Vue 2 中,同一个元素上同时使用 v-for 和 v-if 时,v-for 优先级更高。这导致了一些潜在的性能陷阱(如需要先循环再判断隐藏)。Vue 3 反过来:v-if 优先级高于 v-for。
这意味着 v-if 无法访问 v-for 内的迭代变量。官方强烈建议永远不要将两者放在同一个元素上,最佳实践是:
- 先用
computed过滤列表,再对过滤后的数据v-for - 或者在
v-for外层加一个<template>放置v-if
八、自定义指令钩子函数对齐组件生命周期
自定义指令的钩子名称从 Vue 2 的独立命名(bind、inserted、update 等)改为与组件生命周期对齐的命名。
| Vue 2 | Vue 3 | 触发时机 |
|-------|-------|----------|
| bind | beforeMount | 指令首次绑定到元素时 |
| inserted | mounted | 元素被插入父节点时 |
| update | 移除,用 updated | 组件更新时(钩子可接收新旧值) |
| componentUpdated | updated | 组件及子组件更新后 |
| unbind | unmounted | 指令与元素解绑时 |
同时,钩子的参数也发生了变化:vnode 被替换为当前组件实例 binding.instance。
九、其他常见的细节变化
$listeners被合并到$attrs
Vue 2 中,$attrs 只包含未声明的 props,而 $listeners 包含所有未在 emits 中声明的事件监听器。Vue 3 移除了 $listeners,所有传递下来的 props 和事件(都以 onXxx 形式)都在 $attrs 中。这对封装第三方组件时的属性透传有较大影响。
keyCode按键修饰符移除
不再支持 @keyup.13 这种写法,只能使用官方别名(如 @keyup.enter)。
- 过渡类名修改
CSS 类名从 v-enter、v-leave 改为 v-enter-from、v-leave-from,避免与旧的 v-enter-active 产生语义混淆。
- 渲染函数 API 全面导入
render 函数不再自动注入 h,需手动从 vue 导入;VNode 的属性结构变为扁平化(如不再有 class 对象,而是直接传 class 属性)。
- 移除
$children
在 Vue 2 中可以通过 this.$children 访问所有子组件实例,这在 Vue 3 中已不再提倡。组件通信应使用 props、emits、ref 或 provide/inject。
迁移策略建议
面对这些变更,不必一次性全部改完。官方的 @vue/compat 兼容构建允许在运行时模拟大部分 Vue 2 行为,并在控制台给出废弃警告,帮助渐进式升级。建议流程:
- 使用
@vue/compat将项目升级到 Vue 3 运行时,确保应用能正常启动。 - 根据控制台警告,逐项修复不兼容代码。
- 最终移除兼容包,完成纯净的 Vue 3 构建。
这份总览的目的不是让你把所有 API 背下来,而是在遇到“为什么代码在 Vue 3 里挂了”时,能快速定位到对应的破坏性变更,找到正确的修改方向。下一节将介绍具体迁移工具和分步实施方法。