自 2020 年 Vue 3 正式发布以来,Vue 生态迎来了一次质的飞跃。它不仅带来了更现代化的开发范式,还在性能、TypeScript 集成、代码组织方式上做出了革命性改进。本文将深入对比 Vue 2 与 Vue 3 的核心差异,帮助开发者理解设计动机,并为新项目选型或旧项目升级提供决策参考。
1. 响应式系统:从 defineProperty 到 Proxy
这是底层最根本的变化,几乎影响所有上层功能。
| 对比项 | Vue 2 (Object.defineProperty) |
Vue 3 (Proxy) |
|---|---|---|
| 监听方式 | 递归遍历对象属性,逐个劫持 getter/setter | 代理整个对象,可拦截 13 种基本操作 |
| 数组变化 | 无法直接监听索引/长度变更,需改写 push/pop 等 7 个方法 |
完美支持索引赋值、length 修改等 |
| 新增属性 | 必须使用 Vue.set 或 this.$set |
自动响应,无需额外 API |
| 删除属性 | 必须使用 Vue.delete |
直接 delete 即可触发更新 |
| 性能优势 | 初始递归遍历大对象时较慢 | 懒代理,属性被访问时才递归代理子对象 |
| 限制 | 无法监听 Map/Set/WeakMap/WeakSet |
原生支持所有集合类型 |
kotlin
// Vue 2 痛点示例
data() {
return { user: { name: 'Alice' }, tags: ['a','b'] }
},
methods: {
update() {
this.user.age = 18; // ❌ 非响应式
this.$set(this.user, 'age', 18); // ✅ 必须使用$set
this.tags[0] = 'A'; // ❌ 非响应式
this.tags.splice(0,1,'A'); // ✅ 需用数组变异方法
}
}
// Vue 3 优雅写法
const state = reactive({ user: { name: 'Alice' }, tags: ['a','b'] });
state.user.age = 18; // ✅ 自动响应
state.tags[0] = 'A'; // ✅ 完全响应
delete state.user.name; // ✅ 触发更新
2. 组合式 API vs 选项式 API
这是开发模式上最显著的变化,解决了 Vue 2 中逻辑复用和组织混乱的问题。
Vue 2 问题:
- 同一业务逻辑的代码分散在
data、methods、mounted、watch等不同选项中 - 逻辑复用依赖 mixins,存在命名冲突和来源不清晰的问题
- TypeScript 类型推断困难
Vue 3 解决方案 ------ 组合式 API:
- 按功能逻辑聚合代码,类似 React Hooks
- 更好的逻辑抽离与复用(可组合函数)
- 天然 TypeScript 友好
xml
<!-- Vue 2 选项式 API -->
<script>
export default {
data() {
return { count: 0, msg: '' }
},
methods: {
increment() { this.count++ }
},
mounted() {
console.log('mounted')
}
}
</script>
<!-- Vue 3 组合式 API(<script setup> 语法糖) -->
<script setup>
import { ref, onMounted } from 'vue'
const count = ref(0)
const msg = ref('')
const increment = () => count.value++
onMounted(() => console.log('mounted'))
</script>
逻辑复用对比:
javascript
// Vue 2 mixin(有隐患)
const mouseMixin = {
data: () => ({ x:0, y:0 }),
mounted() { /* 监听鼠标 */ },
destroyed() { /* 清理 */ }
}
// Vue 3 可组合函数(清晰、可追踪)
import { ref, onMounted, onUnmounted } from 'vue'
function useMouse() {
const x = ref(0), y = ref(0)
const update = (e) => { x.value=e.clientX; y.value=e.clientY }
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 在组件中使用
const { x, y } = useMouse()
3. 生命周期钩子变化
| Vue 2 | Vue 3 选项式 | Vue 3 组合式 API |
|---|---|---|
beforeCreate |
beforeCreate |
无需(setup 执行在其之前) |
created |
created |
无需(setup 内可直接执行) |
beforeMount |
beforeMount |
onBeforeMount |
mounted |
mounted |
onMounted |
beforeUpdate |
beforeUpdate |
onBeforeUpdate |
updated |
updated |
onUpdated |
beforeDestroy |
beforeUnmount |
onBeforeUnmount |
destroyed |
unmounted |
onUnmounted |
errorCaptured |
errorCaptured |
onErrorCaptured |
| - | - | onRenderTracked(调试用) |
| - | - | onRenderTriggered(调试用) |
4. 组件模板语法增强
4.1 多根节点组件(Fragment)
Vue 2 要求每个组件必须有唯一的根元素,否则报错。Vue 3 支持多根节点,自动创建 Fragment 包裹,减少无意义的 DOM 层级。
xml
<!-- Vue 3 合法 -->
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
4.2 Teleport(传送门)
相当于 React 的 createPortal,允许将组件内容渲染到 DOM 树的任意位置(如 body),解决 Modal、Toast 等样式覆盖问题。
xml
<template>
<div class="app">
<button @click="open = true">打开弹窗</button>
<!-- 传送门:弹窗实际会渲染到 body 下 -->
<Teleport to="body">
<Modal v-if="open" @close="open = false" />
</Teleport>
</div>
</template>
4.3 内置 Suspense(实验性)
允许组件等待异步依赖(如 setup 中的 async 函数或异步子组件)时展示 fallback 内容。
5. 性能优化
| 优化项 | Vue 2 | Vue 3 |
|---|---|---|
| 初始化性能 | 递归遍历所有数据 | 惰性代理,按需递归 |
| 虚拟 DOM 开销 | 全量 diff | 静态提升 + PatchFlags 标记动态节点 |
| 静态节点处理 | 每次重新创建 | 静态提升,复用同一 VNode |
| 事件监听器 | 每次更新重新绑定 | 缓存事件处理函数 |
| 体积 | 约 32KB(gzipped) | 约 22KB(gzipped,含 Composition API) |
| Tree-shaking | 不支持(全局 API) | 支持(按需引入,未使用的功能不打包) |
编译优化示例(PatchFlags):
xml
<!-- 模板 -->
<div>{{ msg }}</div>
<div class="static">我不会变化</div>
Vue 3 编译器会识别:
- 第二个 div 是纯静态节点 → 提升到渲染函数外,只创建一次
- 第一个 div 只有文本动态 → 打上
TEXT标记,diff 时只比较 textContent
6. TypeScript 集成
Vue 2 对 TS 支持较差(需要装饰器或 vue-class-component,类型推断不完善)。Vue 3 从底层用 TypeScript 重写,提供了完美的类型推断。
xml
// Vue 3 + <script setup> 自动推导
<script setup lang="ts">
import { ref } from 'vue'
// count 类型自动推导为 Ref<number>
const count = ref(0)
// 定义 props 类型
const props = defineProps<{
title: string
initial?: number
}>()
// 定义 emit
const emit = defineEmits<{
(e: 'change', value: number): void
}>()
</script>
7. 全局 API 变更
Vue 2 的全局 API(如 Vue.component、Vue.directive)会在多个应用间污染。Vue 3 引入了 createApp,每个应用都是独立的。
javascript
// Vue 2
import Vue from 'vue'
Vue.component('MyButton', MyButton)
Vue.directive('focus', {...})
new Vue({ render: h => h(App) }).$mount('#app')
// Vue 3
import { createApp } from 'vue'
const app = createApp(App)
app.component('MyButton', MyButton)
app.directive('focus', {...})
app.mount('#app')
其他 API 变化:
Vue.nextTick→import { nextTick } from 'vue'(可作为普通函数使用)Vue.observable→import { reactive } from 'vue'Vue.set/Vue.delete已移除(Proxy 原生支持)$on、$off、$once实例方法已移除(事件总线推荐 mitt 等第三方库)
8. v-model 的演变
Vue 2 中一个组件只能绑定一个 v-model(默认绑定 value prop,触发 input 事件)。Vue 3 可绑定多个,且自定义修饰符更灵活。
xml
<!-- 父组件 -->
<Child v-model="firstName" v-model:lastName="lastName" />
<!-- Child 组件内部 -->
<script setup>
defineProps(['modelValue', 'lastName'])
defineEmits(['update:modelValue', 'update:lastName'])
</script>
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
<input :value="lastName" @input="$emit('update:lastName', $event.target.value)">
</template>
9. 其他重要变更
9.1 异步组件重新定义
Vue 2:() => import('./My.vue') 直接返回函数。
Vue 3:需要使用 defineAsyncComponent 包装,以便支持加载中、错误等状态。
javascript
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./My.vue'),
loadingComponent: LoadingComp,
errorComponent: ErrorComp,
delay: 200,
timeout: 3000
})
9.2 自定义元素白名单
Vue 2 需要通过 ignoredElements 配置忽略自定义元素;Vue 3 改为 compilerOptions.isCustomElement。
9.3 移除的 API/特性
- filters(推荐用 computed 或 method 替代)
$children(使用$refs或手动建立父子关联)$listeners(合并到$attrs,包含 class/style)inline-template(已移除)propsData(使用createApp的第二个参数传递 props)
9.4 新增组件
<Suspense>(实验性)<Teleport><Fragment>(内置支持)
10. 构建工具链:Vue CLI vs Vite
Vue 2 官方推荐 Vue CLI(基于 Webpack),启动和热更新较慢。Vue 3 官方推荐 Vite ------ 基于原生 ES modules 的开发服务器,启动速度极快(毫秒级),HMR 即时生效。
csharp
# 创建 Vue 3 + Vite 项目
npm init vue@latest
Vite 不仅速度快,还原生支持 TypeScript、JSX、CSS 预处理器,并提供插件生态。当然,Vue 3 项目仍可以使用 Vue CLI(Webpack),但 Vite 已是官方推荐。
11. 迁移策略与建议
11.1 新项目:毫不犹豫选 Vue 3 + Vite + <script setup>
- 性能更好、体积更小、TS 支持完美、开发体验更佳
- 组合式 API 让复杂组件逻辑清晰可维护
- 未来 Vue 2 将进入维护阶段(2023 年底结束 LTS)
11.2 老项目升级
官方提供 迁移构建版本 (@vue/compat),可兼容 Vue 2 代码并提示不兼容点。建议渐进式升级:
- 使用迁移构建,修复所有警告
- 逐步将选项式 API 改写为组合式 API(非必须,但推荐)
- 替换全局 API(
Vue.component等为app.component) - 移除已废弃特性(filters、
$children等) - 升级构建工具到 Vite 或调整 Webpack 配置
对于复杂老项目,若团队人力有限且功能稳定,也可继续使用 Vue 2,但需关注安全更新。
11.3 共存与兼容库
- vue-demi:允许同一个库同时支持 Vue 2 和 Vue 3
- @vue/composition-api:Vue 2 插件,让 Vue 2 也能使用组合式 API
总结
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | defineProperty(有缺陷) |
Proxy(完美) |
| 代码组织 | 选项式 API(分散) | 组合式 API(聚合) |
| TS 支持 | 较弱 | 一流 |
| 性能 | 良好 | 优异(体积小、编译优化) |
| 新特性 | 无 Fragment/Teleport | Fragment/Teleport/Suspense |
| 工具链 | Vue CLI (Webpack) | Vite (极速) |
| 学习曲线 | 平缓 | 略有增加(组合式 API 需适应) |
Vue 3 并非简单的版本迭代,而是一次架构重塑。它吸收了 React Hooks 的优点,同时保持了 Vue 特有的模板直观性和低上手门槛。无论你是个人开发者还是团队负责人,现在都应当将 Vue 3 作为首选。Vue 2 的辉煌即将落幕,而 Vue 3 的时代正全面到来。