Vue2 与 Vue3 全面对比
Vue2 与 Vue3 全面对比
Vue3 于 2020 年 9 月正式发布(代号 One Piece),2022 年 2 月起成为默认版本,Vue2 已于 2023 年 12 月 31 日停止官方维护。相比 Vue2,Vue3 在性能、体积、API 设计、TypeScript 支持等方面带来了许多重大改进。本文从多个维度全面对比 Vue2 和 Vue3,帮助开发者理解两者的区别,并为迁移提供参考。
零、为什么要关心两者的区别
即使今天启动新项目一定会选 Vue3,理解两者区别依然至关重要,原因有三:
可以用一个类比:Vue2 像是一台功能齐全但零件焊死的一体机,Vue3 则是模块化的组装机——你只装需要的模块,性能更好、更容易扩展和维护。
一、性能对比
1. 打包体积优化
Vue2:
Vue3:
// Vue2:全部导入
import Vue from 'vue';
// Vue3:按需导入
import { ref, reactive, computed } from 'vue';为什么 Vue3 能 Tree Shaking? 因为 Vue3 把全局 API 从 Vue 对象上的方法改成了独立导出的具名函数。打包工具(如 Rollup/Vite)能静态分析出哪些函数没被引用并删除。而 Vue2 里 Vue.nextTick、Vue.set 都挂在 Vue 对象上,无法被安全删除。
体积对比数据:
| 场景 | Vue2 | Vue3 | 变化 |
|------|------|------|------|
| 运行时核心(gzip) | 约 22KB | 约 10KB | -55% |
| 只用 ref/computed 的最小应用 | 约 22KB | 约 13.5KB | 显著更小 |
| Transition 等未使用特性 | 打包进去 | 被摇树删除 | 按需 |
2. 渲染性能
Vue2:
Vue3:
// Vue2 编译结果
function render() {
return h('div', { id: 'app' }, [
h('h1', {}, this.title),
h('p', { class: 'static' }, 'Static text')
]);
}
// Vue3 编译结果(带优化)
const _hoisted_1 = h('p', { class: 'static' }, 'Static text');
function render() {
return h('div', { id: 'app' }, [
h('h1', {}, this.title),
_hoisted_1
]);
}补丁标志(Patch Flags)示例: Vue3 编译器会给动态节点打上标记,运行时只对比标记的部分:
// 模板:<div :id="dynamicId" class="static">{{ text }}</div>
// 编译后(简化)
createElementVNode("div", {
id: dynamicId,
class: "static"
}, toDisplayString(text), 9 /* TEXT, PROPS */, ["id"])
// 数字 9 是 patchFlag,["id"] 表示只有 id 是动态 prop
// 更新时 class 完全跳过对比,只看 id 和文本渲染性能对比(官方基准,10000 个组件):
| 指标 | Vue2 | Vue3 | 提升 |
|------|------|------|------|
| 初次渲染 | 基准 | 约快 1.3-2 倍 | 更快 |
| 更新渲染 | 基准 | 约快 1.3-2 倍 | 更快 |
| 内存占用 | 基准 | 约减少一半 | 更省 |
3. 响应式性能
Vue2(Object.defineProperty):
Vue3(Proxy):
Vue2 响应式的经典局限演示:
// Vue2 中这些操作不会触发视图更新
this.obj.newProp = 'value'; // 新增属性无效
delete this.obj.oldProp; // 删除属性无效
this.arr[0] = 'newValue'; // 索引赋值无效
this.arr.length = 0; // 修改 length 无效
// 必须使用特殊 API
this.$set(this.obj, 'newProp', 'value');
this.$delete(this.obj, 'oldProp');
this.arr.splice(0, 1, 'newValue');// Vue3 中一切照常工作
const obj = reactive({ a: 1 });
obj.newProp = 'value'; // 有效,视图更新
delete obj.a; // 有效
const arr = reactive([1, 2, 3]);
arr[0] = 'newValue'; // 有效
arr.length = 0; // 有效为什么 Proxy 更强? Object.defineProperty 只能拦截"已存在的、被明确 define 过的属性"的读写;Proxy 是拦截"整个对象"的操作,包括属性新增、删除、in 判断等 13 种拦截操作,因此天然支持动态属性。
二、API 设计对比
1. Options API vs Composition API
Vue2 - Options API:
按选项(data、methods、computed、watch)组织代码。
<script>
export default {
data() {
return {
count: 0,
user: null
};
},
methods: {
increment() {
this.count++;
}
},
computed: {
doubleCount() {
return this.count * 2;
}
},
mounted() {
this.fetchUser();
}
};
</script>Vue3 - Composition API:
按功能组织代码,相关逻辑集中在一起。
<script setup>
import { ref, computed, onMounted } from 'vue';
const count = ref(0);
const user = ref(null);
const increment = () => {
count.value++;
};
const doubleCount = computed(() => count.value * 2);
const fetchUser = async () => {
// 获取用户
};
onMounted(() => {
fetchUser();
});
</script>核心差异的类比:Options API 像是把所有"工具按种类分抽屉"(螺丝刀一个抽屉、螺丝一个抽屉),修一个东西要开好几个抽屉;Composition API 像是把"修某个东西需要的所有工具装进一个工具箱",逻辑内聚。当组件变大时,Options API 的同一功能会散落在 data、methods、computed、watch 各处,而 Composition API 能把它们收拢在一起。
对比分析:
| 特性 | Options API | Composition API |
|------|-------------|-----------------|
| 代码组织 | 按选项分散 | 按功能集中 |
| 逻辑复用 | mixins | composables |
| TypeScript 支持 | 一般 | 优秀 |
| 学习曲线 | 低 | 较高 |
| 大组件可维护性 | 差(逻辑分散) | 好(逻辑内聚) |
| this 指向 | 依赖 this | 无 this,更直观 |
2. 逻辑复用:mixins vs composables
Vue2 mixins 的问题:
// mixins/counter.js
export default {
data() {
return { count: 0 };
},
methods: {
increment() { this.count++; }
}
};
// 使用
import counterMixin from './mixins/counter';
export default {
mixins: [counterMixin]
// 问题:count 和 increment 从哪来?不看 mixin 源码根本不知道
// 多个 mixin 还可能命名冲突
};Vue3 composables 的优势:
// composables/useCounter.js
import { ref } from 'vue';
export function useCounter(initial = 0) {
const count = ref(initial);
const increment = () => count.value++;
return { count, increment };
}
// 使用
import { useCounter } from './composables/useCounter';
// 来源清晰,可重命名,无隐式注入
const { count, increment } = useCounter(10);mixins vs composables 对比:
| 问题 | mixins | composables |
|------|--------|-------------|
| 数据来源 | 不清晰(隐式注入) | 清晰(显式解构) |
| 命名冲突 | 容易发生 | 可自行重命名解决 |
| 多个复用 | 难以追踪 | 一目了然 |
| TS 类型推断 | 差 | 好 |
3. 生命周期对比
| Vue2 | Vue3 | 说明 |
|------|------|------|
| beforeCreate | setup() | 实例创建前 |
| created | setup() | 实例创建后 |
| beforeMount | onBeforeMount | 挂载前 |
| mounted | onMounted | 挂载后 |
| beforeUpdate | onBeforeUpdate | 更新前 |
| updated | onUpdated | 更新后 |
| beforeDestroy | onBeforeUnmount | 卸载前 |
| destroyed | onUnmounted | 卸载后 |
注意 Vue3 把 beforeDestroy/destroyed 更名为 onBeforeUnmount/onUnmounted,语义更准确(组件是"卸载"而非"销毁")。beforeCreate 和 created 被 setup() 取代,因为 setup 本身就运行在这两个时机之间。
// Vue2 生命周期
export default {
beforeCreate() {
console.log('beforeCreate');
},
created() {
console.log('created');
},
mounted() {
console.log('mounted');
},
beforeDestroy() {
console.log('beforeDestroy');
}
};
// Vue3 生命周期
import {
onBeforeMount,
onMounted,
onBeforeUnmount,
onUnmounted
} from 'vue';
export default {
setup() {
onBeforeMount(() => {
console.log('onBeforeMount');
});
onMounted(() => {
console.log('onMounted');
});
onBeforeUnmount(() => {
console.log('onBeforeUnmount');
});
onUnmounted(() => {
console.log('onUnmounted');
});
}
};Vue3 新增的调试钩子:
import { onRenderTracked, onRenderTriggered } from 'vue';
export default {
setup() {
// 追踪哪个响应式依赖被收集
onRenderTracked((e) => {
console.log('tracked:', e);
});
// 追踪哪个依赖变化触发了重渲染,排查性能问题利器
onRenderTriggered((e) => {
console.log('triggered:', e);
});
}
};三、响应式 API 对比
1. 数据定义
Vue2:
export default {
data() {
return {
count: 0,
user: {
name: 'Alice',
age: 25
}
};
}
};Vue3:
import { ref, reactive } from 'vue';
// ref:适合基本类型
const count = ref(0);
// reactive:适合对象
const user = reactive({
name: 'Alice',
age: 25
});ref vs reactive 该用哪个?
| 维度 | ref | reactive |
|------|-----|----------|
| 适用类型 | 任意(基本类型/对象) | 仅对象/数组/集合 |
| 访问方式 | .value(模板中自动解包) | 直接访问属性 |
| 解构 | 保持响应性 | 解构会丢失响应性 |
| 整体替换 | 可以(count.value = 新对象) | 不可直接替换整个对象 |
// reactive 解构丢失响应性的坑
const state = reactive({ count: 0 });
let { count } = state; // count 只是普通值,不再响应
count++; // state.count 不变
// 解决方案:toRefs
import { toRefs } from 'vue';
const { count } = toRefs(state); // count 是 ref,保持响应实践建议:优先用 ref,它更统一(任何类型都能包),只在明确管理一组相关状态时用 reactive。
2. 计算属性
Vue2:
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
};
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
};Vue3:
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value;
});可写计算属性(Vue3):
const fullName = computed({
get() {
return firstName.value + ' ' + lastName.value;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ');
}
});
fullName.value = 'Jane Smith'; // 会拆分并回写3. 监听器
Vue2:
export default {
data() {
return {
count: 0
};
},
watch: {
count(newVal, oldVal) {
console.log('count changed:', newVal, oldVal);
}
}
};Vue3:
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
// 监听单个源
watch(count, (newVal, oldVal) => {
console.log('count changed:', newVal, oldVal);
});
// watchEffect:自动追踪依赖
watchEffect(() => {
console.log('count:', count.value);
});watch 的更多用法:
// 监听多个源
watch([firstName, lastName], ([newFirst, newLast]) => {
console.log(newFirst, newLast);
});
// 监听 reactive 对象的某个属性(用 getter)
watch(() => user.age, (newAge) => {
console.log('年龄变化', newAge);
});
// 深度监听 + 立即执行
watch(user, (val) => {
console.log('user 深层变化', val);
}, { deep: true, immediate: true });watch vs watchEffect 对比:
| 维度 | watch | watchEffect |
|------|-------|-------------|
| 依赖声明 | 显式指定 | 自动收集 |
| 首次执行 | 默认不执行 | 立即执行 |
| 拿到旧值 | 能 | 不能 |
| 适用场景 | 精确监听特定源 | 副作用随任意依赖自动运行 |
四、组件系统对比
1. 组件定义
Vue2:
// 全局注册
Vue.component('my-component', {
template: '<div>My Component</div>'
});
// 局部注册
export default {
components: {
MyComponent
}
};Vue3:
import { defineComponent } from 'vue';
import MyComponent from './MyComponent.vue';
// defineComponent(可选,用于类型推断)
export default defineComponent({
components: {
MyComponent
}
});
// <script setup> 自动注册
<script setup>
import MyComponent from './MyComponent.vue';
</script>全局注册的差异:
// Vue2:挂在全局 Vue 上,影响所有实例
Vue.component('MyComponent', MyComponent);
// Vue3:挂在具体 app 实例上,多个 app 互不影响
const app = createApp(App);
app.component('MyComponent', MyComponent);Vue3 把全局配置从"全局单例 Vue"改为"每个 createApp 实例独立",解决了 Vue2 中"一处配置污染全局、多实例互相干扰"的问题。
2. Props 和 Emits
Vue2:
<script>
export default {
props: {
title: String,
count: {
type: Number,
default: 0
}
},
methods: {
handleClick() {
this.$emit('click', { id: 1 });
}
}
};
</script>Vue3:
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
});
const emit = defineEmits(['click', 'update']);
const handleClick = () => {
emit('click', { id: 1 });
};
</script>基于 TypeScript 的类型化 Props(Vue3 独有):
<script setup lang="ts">
interface Props {
title: string;
count?: number;
}
// 类型驱动的声明,配合 withDefaults 设默认值
const props = withDefaults(defineProps<Props>(), {
count: 0
});
const emit = defineEmits<{
(e: 'click', payload: { id: number }): void;
(e: 'update', value: string): void;
}>();
</script>这是 Vue3 相比 Vue2 在开发体验上的巨大飞跃:props 和 emits 都有完整的类型检查和自动补全。
3. Vue3 新增的内置组件
<!-- Fragment:Vue3 支持多个根节点,Vue2 必须单根 -->
<template>
<header>头部</header>
<main>内容</main>
<footer>底部</footer>
</template><!-- Teleport:把内容渲染到 DOM 树的其他位置,如 body 下的弹窗 -->
<template>
<Teleport to="body">
<div class="modal">我被传送到 body 下了</div>
</Teleport>
</template><!-- Suspense:优雅处理异步组件的加载态 -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>五、状态管理对比
Vuex 3 vs Vuex 4 vs Pinia
Vuex 3(Vue2):
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
increment({ commit }) {
commit('increment');
}
}
});Vuex 4(Vue3):
import { createStore } from 'vuex';
export default createStore({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
increment({ commit }) {
commit('increment');
}
}
});Pinia(Vue3 推荐):
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++;
}
},
getters: {
doubleCount: (state) => state.count * 2
}
});
// 组件中使用
const counterStore = useCounterStore();
counterStore.increment();Pinia 的组合式写法(更贴近 Composition API):
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});为什么 Pinia 取代 Vuex 成为官方推荐?
| 维度 | Vuex 4 | Pinia |
|------|--------|-------|
| mutations | 必须有,样板代码多 | 取消,直接改 state |
| TypeScript | 支持差,类型推断难 | 一流的类型支持 |
| 模块化 | 需 modules 嵌套 | 天然扁平,多 store |
| 体积 | 较大 | 约 1KB |
| DevTools | 支持 | 支持,且更好 |
| API 心智 | state/getters/mutations/actions 四段 | 去掉 mutations,更简洁 |
六、Vue Router 对比
Vue Router 3(Vue2):
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
component: Home
}
]
});Vue Router 4(Vue3):
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: Home
}
]
});Router 3 与 4 的关键差异:
| 差异点 | Router 3 | Router 4 |
|--------|----------|----------|
| 实例创建 | new VueRouter() | createRouter() |
| 模式配置 | mode: 'history' | history: createWebHistory() |
| 组合式 API | 无 | useRoute() / useRouter() |
| 守卫 next | 必须调用 | 可用返回值代替 |
| 通配符 | path: '' | path: '/:pathMatch(.)*' |
组合式路由 API(Router 4 新增):
<script setup>
import { useRoute, useRouter } from 'vue-router';
const route = useRoute(); // 相当于 this.$route
const router = useRouter(); // 相当于 this.$router
console.log(route.params.id);
router.push('/home');
</script>七、迁移指南
从 Vue2 迁移到 Vue3
1. 更新依赖:
{
"dependencies": {
"vue": "^3.0.0",
"vue-router": "^4.0.0",
"vuex": "^4.0.0"
}
}2. 全局 API 变更:
// Vue2
import Vue from 'vue';
Vue.use(Plugin);
Vue.component('MyComponent', MyComponent);
// Vue3
import { createApp } from 'vue';
const app = createApp(App);
app.use(Plugin);
app.component('MyComponent', MyComponent);
app.mount('#app');3. 模板变更:
<!-- Vue2 .sync -->
<Child :title.sync="title" />
<!-- Vue3 等价写法 -->
<Child v-model:title="title" />4. 移除的特性:
// Vue2 过滤器
{{ price | currency }}
// Vue3 用计算属性或方法替代
{{ formatCurrency(price) }}5. 迁移工具与策略:
迁移成本评估参考
| 项目规模 | 组件数 | 预估迁移工时 | 建议策略 |
|----------|--------|--------------|----------|
| 小型 | < 30 | 1-2 周 | 一次性重写 |
| 中型 | 30-150 | 1-2 月 | compat 渐进迁移 |
| 大型 | > 150 | 3-6 月 | 新功能上 Vue3,旧模块逐步迁 |
八、最佳实践建议
1. 新项目:
2. 老项目迁移:
3. 代码组织:
4. 一个组合式逻辑复用的真实案例(鼠标位置追踪):
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(e) {
x.value = e.pageX;
y.value = e.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
// 任何组件里一行复用,且自动清理事件监听
// const { x, y } = useMouse();这种"逻辑 + 生命周期"打包复用的能力,是 Vue2 的 mixins 难以优雅实现的,也是 Composition API 最大的价值所在。
十、生命周期钩子完整对照
Vue3 的生命周期钩子在 Composition API 中需要显式导入,命名前加 on,且大部分与 Vue2 一一对应,但有几个关键变化。
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
onActivated, onDeactivated,
onErrorCaptured, onRenderTracked, onRenderTriggered
} from 'vue';
// 在 setup 中注册
onMounted(() => {
console.log('组件已挂载');
});Vue2 与 Vue3 生命周期对照表:
| Vue2(Options) | Vue3(Options) | Vue3(Composition) | 说明 |
|------------------|------------------|----------------------|------|
| beforeCreate | beforeCreate | setup() | setup 取代 |
| created | created | setup() | setup 取代 |
| beforeMount | beforeMount | onBeforeMount | 挂载前 |
| mounted | mounted | onMounted | 挂载后 |
| beforeUpdate | beforeUpdate | onBeforeUpdate | 更新前 |
| updated | updated | onUpdated | 更新后 |
| beforeDestroy | beforeUnmount | onBeforeUnmount | 改名 |
| destroyed | unmounted | onUnmounted | 改名 |
| activated | activated | onActivated | keep-alive 激活 |
| deactivated | deactivated | onDeactivated | keep-alive 停用 |
| errorCaptured | errorCaptured | onErrorCaptured | 错误捕获 |
关键变化:
// 调试钩子:追踪是哪个响应式数据触发了重渲染
onRenderTriggered((event) => {
console.log('触发重渲染的数据:', event.key, event.type);
});父子组件生命周期执行顺序(挂载):
父 beforeCreate/setup → 父 beforeMount → 子 beforeCreate/setup → 子 beforeMount → 子 mounted → 父 mounted。即"父组件先开始,子组件先完成"。
十一、响应式 API 深度对比
这是 Vue2 到 Vue3 变化最大的部分,直接决定了写法差异。
1. ref 与 reactive
import { ref, reactive } from 'vue';
// ref:适合基本类型,访问需 .value
const count = ref(0);
count.value++;
// reactive:适合对象,直接访问属性
const state = reactive({ count: 0, list: [] });
state.count++;ref vs reactive 对比:
| 维度 | ref | reactive |
|------|-----|----------|
| 适用类型 | 任意(含基本类型) | 仅对象/数组/Map/Set |
| 访问方式 | .value | 直接访问 |
| 模板中 | 自动解包,无需 .value | 直接用 |
| 解构 | 保持响应式 | 丢失响应式(需 toRefs) |
| 重新赋值 | 可整体替换 | 不能替换整个对象 |
为什么 reactive 不能整体替换?
let state = reactive({ count: 0 });
// 反例:整体替换会断开响应式连接(Proxy 换了个新对象)
state = reactive({ count: 10 }); // 视图不更新
// 正例:改属性,或用 ref
const state2 = ref({ count: 0 });
state2.value = { count: 10 }; // 可以整体替换2. toRef 与 toRefs
解构 reactive 对象会丢失响应式,用 toRefs 保持。
import { reactive, toRefs, toRef } from 'vue';
const state = reactive({ name: 'Alice', age: 20 });
// 反例:解构后 name、age 是普通值,不再响应式
const { name, age } = state;
// 正例:toRefs 把每个属性转成 ref
const { name: nameRef, age: ageRef } = toRefs(state);
// toRef:只转单个属性
const nameOnly = toRef(state, 'name');这在 composable 返回值时特别有用,让调用方可以解构而不丢响应式:
function useUser() {
const state = reactive({ name: '', loading: false });
return { ...toRefs(state) }; // 调用方可安全解构
}3. computed 对比
// Vue2 Options
export default {
computed: {
fullName() { return this.first + this.last; }
}
};
// Vue3 Composition
import { computed, ref } from 'vue';
const first = ref('A'), last = ref('B');
const fullName = computed(() => first.value + last.value);
// 可写 computed
const fullName2 = computed({
get: () => first.value + ' ' + last.value,
set: (val) => { [first.value, last.value] = val.split(' '); }
});4. watch 与 watchEffect
import { watch, watchEffect, ref } from 'vue';
const count = ref(0);
// watch:明确指定监听源,能拿到新旧值,惰性
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal);
});
// 监听多个源
watch([a, b], ([na, nb], [oa, ob]) => {});
// 监听 reactive 对象的属性用 getter
watch(() => state.count, (val) => {});
// watchEffect:自动收集依赖,立即执行,不能拿旧值
watchEffect(() => {
console.log('count 是', count.value); // 依赖 count,count 变就重跑
});watch vs watchEffect:
| 维度 | watch | watchEffect |
|------|-------|-------------|
| 依赖声明 | 显式指定 | 自动收集 |
| 首次执行 | 惰性(immediate 可改) | 立即执行 |
| 新旧值 | 都能拿到 | 只有当前值 |
| 适用 | 需对比新旧、精确控制 | 副作用随依赖自动重跑 |
5. Vue3 新增的响应式工具
import { shallowRef, shallowReactive, readonly, toRaw, markRaw, isRef, unref } from 'vue';
const s = shallowRef({ a: 1 }); // 只有 .value 替换才响应,内部不深层代理
const sr = shallowReactive({}); // 只有第一层响应
const ro = readonly(state); // 只读代理,改会警告
const raw = toRaw(state); // 拿到原始对象
const noReact = markRaw({}); // 标记永不代理
console.log(isRef(s)); // 判断是否 ref
console.log(unref(s)); // 是 ref 返回 .value,否则返回本身十二、组件通信方式对比
1. props 与自定义事件
// Vue2 子组件
export default {
props: ['title'],
methods: {
submit() { this.$emit('submit', data); }
}
};
// Vue3 script setup
const props = defineProps(['title']);
const emit = defineEmits(['submit']);
function submit() { emit('submit', data); }2. v-model 的重大变化
// Vue2:默认 prop 是 value,事件是 input;只能一个 v-model
// <Child v-model="msg" /> 等价于 :value + @input
// Vue3:默认 prop 是 modelValue,事件是 update:modelValue;支持多个
// <Child v-model="msg" /> 等价于 :modelValue + @update:modelValue
// <Child v-model:title="t" v-model:content="c" /> 多个 v-model<!-- Vue3 自定义组件支持多个 v-model -->
<script setup>
defineProps(['title', 'content']);
const emit = defineEmits(['update:title', 'update:content']);
</script>
<template>
<input :value="title" @input="emit('update:title', $event.target.value)" />
<textarea :value="content" @input="emit('update:content', $event.target.value)" />
</template>注意: Vue2 的 .sync 修饰符在 Vue3 中被移除,其功能被多个 v-model 取代。
| 特性 | Vue2 | Vue3 |
|------|------|------|
| 默认 prop | value | modelValue |
| 默认事件 | input | update:modelValue |
| 多个 v-model | 不支持(用 .sync) | 支持 |
| .sync 修饰符 | 支持 | 移除,用 v-model:xxx |
3. provide / inject
// Vue3 组合式,配合 ref 保持响应式
import { provide, inject, ref, readonly } from 'vue';
// 祖先
const theme = ref('dark');
provide('theme', readonly(theme)); // readonly 防止后代修改
// 后代
const theme = inject('theme', 'light'); // 第二个参数是默认值十三、全局 API 的变化
Vue3 把全局 API 从 Vue 构造函数移到了应用实例 app 上,避免全局污染,支持多个应用实例。
// Vue2:全局 API 挂在 Vue 上,影响所有实例
import Vue from 'vue';
Vue.component('MyComp', {});
Vue.directive('focus', {});
Vue.use(plugin);
Vue.mixin({});
Vue.prototype.$http = axios;
new Vue({ render: h => h(App) }).$mount('#app');
// Vue3:挂在 app 实例上,隔离作用域
import { createApp } from 'vue';
const app = createApp(App);
app.component('MyComp', {});
app.directive('focus', {});
app.use(plugin);
app.mixin({});
app.config.globalProperties.$http = axios; // 取代 Vue.prototype
app.mount('#app');全局 API 对照表:
| Vue2 | Vue3 |
|------|------|
| new Vue() | createApp() |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.use | app.use |
| Vue.mixin | app.mixin |
| Vue.prototype.$x | app.config.globalProperties.$x |
| Vue.nextTick | import { nextTick } |
| Vue.observable | import { reactive } |
被移除或改变的全局 API:
十四、内置组件的新增与变化
Vue3 新增了几个强大的内置组件,解决了 Vue2 中的痛点。
1. Fragment:多根节点
Vue2 要求组件模板必须有唯一根节点,常常被迫包一层无意义的 div。Vue3 支持多根节点(Fragment)。
<!-- Vue2:必须单根,被迫包 div -->
<template>
<div>
<header>头</header>
<main>主体</main>
</div>
</template>
<!-- Vue3:可以多根,减少无意义嵌套 -->
<template>
<header>头</header>
<main>主体</main>
<footer>脚</footer>
</template>注意: 多根组件的属性透传($attrs)需要用 inheritAttrs 或手动 v-bind="$attrs" 指定落到哪个根节点,否则会警告。
2. Teleport:传送门
把组件内部的 DOM 渲染到组件树之外的位置,完美解决弹窗、提示框被父级 overflow/z-index 限制的问题。
<template>
<button @click="open = true">打开弹窗</button>
<!-- 内容渲染到 body 下,逻辑仍在当前组件 -->
<Teleport to="body">
<div v-if="open" class="modal">
<p>我被传送到了 body 下</p>
<button @click="open = false">关闭</button>
</div>
</Teleport>
</template>在 Vue2 里实现同样效果需要第三方库或手动操作 DOM,非常繁琐。
3. Suspense:异步依赖协调
统一处理异步组件和 async setup 的加载态,避免多个 loading 分别闪现。
<template>
<Suspense>
<template #default>
<AsyncUserProfile /> <!-- 内部有 async setup -->
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>// 子组件可以用顶层 await,Suspense 会等它完成
// AsyncUserProfile.vue <script setup>
const user = await fetchUser(); // 顶层 await内置组件对比:
| 组件 | Vue2 | Vue3 |
|------|------|------|
| 多根节点 | 不支持 | 支持(Fragment) |
| Teleport | 无(需第三方) | 内置 |
| Suspense | 无 | 内置(实验性) |
| keep-alive | 有 | 有(含 include/exclude/max) |
| transition | 有 | 有(class 命名微调) |
十五、模板语法的细节变化
1. v-if 与 v-for 优先级反转
<!-- Vue2:v-for 优先级高于 v-if,同元素上 v-if 每次循环都判断 -->
<!-- Vue3:v-if 优先级高于 v-for,此时 v-if 访问不到 v-for 的变量 -->
<!-- 两个版本都推荐:不要在同一元素同时用,改用计算属性或包一层 -->
<template v-for="item in list" :key="item.id">
<li v-if="item.active">{{ item.name }}</li>
</template>更好的做法是用 computed 预先过滤:
const activeList = computed(() => list.value.filter((i) => i.active));2. v-for 中 key 的位置
Vue3 中 key 应放在 template 标签上(Vue2 需放在真实元素上)。
<!-- Vue3 -->
<template v-for="item in list" :key="item.id">
<div>{{ item.a }}</div>
<div>{{ item.b }}</div>
</template>3. v-bind 合并行为
Vue3 中 v-bind="obj" 与单独的属性合并时,顺序决定优先级(后者覆盖前者),Vue2 行为不一致。
<!-- id="red" 会生效(写在后面覆盖 obj 里的 id) -->
<div v-bind="{ id: 'blue' }" id="red"></div>十六、自定义指令 API 的变化
Vue3 让自定义指令的钩子函数名与组件生命周期对齐,更好记。
// Vue2 自定义指令
Vue.directive('focus', {
bind(el) {},
inserted(el) { el.focus(); },
update(el) {},
componentUpdated(el) {},
unbind(el) {}
});
// Vue3 自定义指令(钩子名与生命周期一致)
app.directive('focus', {
created(el) {},
beforeMount(el) {},
mounted(el) { el.focus(); },
beforeUpdate(el) {},
updated(el) {},
beforeUnmount(el) {},
unmounted(el) {}
});指令钩子对照:
| Vue2 | Vue3 |
|------|------|
| bind | beforeMount |
| inserted | mounted |
| update | 移除(用 updated) |
| componentUpdated | updated |
| unbind | unmounted |
十七、事件 API 的移除与替代
Vue3 移除了实例上的 $on、$off、$once,因此不能再用 Vue 实例充当 EventBus。
// Vue2 的 EventBus 写法(Vue3 已不可用)
const bus = new Vue();
bus.$on('event', handler);
bus.$emit('event', data);
// Vue3 替代方案 1:用 mitt 库
import mitt from 'mitt';
export const emitter = mitt();
emitter.on('event', handler);
emitter.emit('event', data);
emitter.off('event', handler);
// Vue3 替代方案 2:优先用 props/emit、provide/inject、Pinia注意: 组件模板里的 @event(v-on)依然正常,移除的只是实例方法 $on/$off/$once。官方更推荐用状态管理(Pinia)替代全局事件总线,因为 EventBus 的数据流难以追踪。
十八、过滤器(filter)的移除
Vue2 的过滤器在 Vue3 中被彻底移除,用方法或计算属性替代。
<!-- Vue2 过滤器 -->
<span>{{ price | currency }}</span>
<!-- Vue3:用方法 -->
<span>{{ formatCurrency(price) }}</span>
<script setup>
function formatCurrency(val) {
return '¥' + val.toFixed(2);
}
</script>全局过滤器可以改为在 app.config.globalProperties 上挂方法,或抽成工具函数导入使用(更利于 Tree Shaking 和类型推导)。
十九、异步组件的定义变化
// Vue2 异步组件
const Async = () => ({
component: import('./Comp.vue'),
loading: LoadingComp,
error: ErrorComp,
delay: 200,
timeout: 3000
});
// Vue3 必须用 defineAsyncComponent 包裹
import { defineAsyncComponent } from 'vue';
const Async = defineAsyncComponent({
loader: () => import('./Comp.vue'),
loadingComponent: LoadingComp,
errorComponent: ErrorComp,
delay: 200,
timeout: 3000
});
// 简单场景
const Simple = defineAsyncComponent(() => import('./Comp.vue'));二十、渲染机制与编译优化差异
这是 Vue3 性能提升的根本来源。
1. 虚拟 DOM 与 diff 优化
Vue2 更新时会对整棵 VNode 树做全量 diff;Vue3 通过编译时标记,只 diff 动态节点。
// Vue3 编译产物(简化)
function render() {
return (openBlock(), createElementBlock('div', null, [
createElementVNode('span', null, '静态文本'), // 静态,不参与 diff
createElementVNode('span', null, toDisplayString(msg), 1 /* TEXT */)
// PatchFlag=1 标记只有文本会变,diff 只看文本
]))
}2. 三大编译优化
| 优化 | 作用 | 效果 |
|------|------|------|
| 静态提升 hoistStatic | 静态节点只创建一次 | 减少重复创建 |
| PatchFlag | 标记动态节点变化类型 | 精准 diff |
| Block Tree | 收集动态节点扁平化 | 跳过静态层级 |
3. 性能数据对比
| 指标 | Vue2 | Vue3 | 提升 |
|------|------|------|------|
| 初次渲染 | 基准 | 更快 | 约 55% |
| 更新渲染 | 基准 | 更快 | 约 133% |
| 内存占用 | 基准 | 更省 | 约 54% |
| SSR 速度 | 基准 | 更快 | 2~3 倍 |
| 打包体积(gzip) | 约 23KB | 约 10KB | 减小超一半 |
二十一、TypeScript 支持对比
Vue3 整体用 TypeScript 重写,类型支持是 Vue2 无法比拟的。
// Vue3 script setup + TS,类型推导完整
<script setup lang="ts">
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), { count: 0 });
const emit = defineEmits<{
(e: 'change', value: number): void;
(e: 'submit', payload: { id: number }): void;
}>();
// ref 自动推导类型
const list = ref<string[]>([]);
</script>TypeScript 支持对比:
| 维度 | Vue2 | Vue3 |
|------|------|------|
| 源码语言 | Flow | TypeScript |
| 组件类型推导 | 需 Vue.extend、体验差 | 原生支持、体验好 |
| props 类型 | 运行时校验为主 | 编译时 + 运行时 |
| emit 类型 | 无 | 完整类型 |
| 模板类型检查 | 弱 | 强(Volar/vue-tsc) |
| IDE 提示 | 一般 | 优秀 |
二十二、状态管理:Vuex vs Pinia
Vue3 官方推荐 Pinia 取代 Vuex。
// Vuex 3/4:概念多(state/getters/mutations/actions/modules)
const store = new Vuex.Store({
state: { count: 0 },
mutations: { INCREMENT(state) { state.count++; } },
actions: { increment({ commit }) { commit('INCREMENT'); } },
getters: { double: (s) => s.count * 2 }
});
// Pinia:去掉 mutations,直接改 state
import { defineStore } from 'pinia';
export const useCounter = defineStore('counter', {
state: () => ({ count: 0 }),
getters: { double: (s) => s.count * 2 },
actions: { increment() { this.count++; } }
});Vuex vs Pinia 对比:
| 维度 | Vuex | Pinia |
|------|------|-------|
| mutations | 必须 | 无 |
| 概念数量 | 多 | 少 |
| TypeScript | 弱 | 完整推导 |
| 模块化 | 嵌套 modules | 扁平多 store |
| 组合式 API | 不友好 | 原生支持 |
| 体积 | 约 10KB | 约 1KB |
| Devtools | 支持 | 支持 |
二十三、script setup 语法糖详解
script setup 是 Vue3 推荐的写法,相比普通 setup() 更简洁:无需手动 return,组件、变量、函数自动暴露给模板。
<!-- 普通 setup():需要 return -->
<script>
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
const inc = () => count.value++;
return { count, inc }; // 必须 return 才能在模板用
}
};
</script>
<!-- script setup:自动暴露,无需 return -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
const inc = () => count.value++;
</script>编译宏
script setup 中可用一系列编译宏(无需导入,编译时处理):
<script setup>
// 定义 props
const props = defineProps({ title: String });
// 定义事件
const emit = defineEmits(['submit']);
// 暴露给父组件(默认 script setup 组件是封闭的)
defineExpose({ focus, reset });
// TS 中带默认值的 props
// const props = withDefaults(defineProps<{ size?: string }>(), { size: 'md' });
// 定义组件选项(如 name、inheritAttrs)
defineOptions({ name: 'MyComponent', inheritAttrs: false });
// 双向绑定语法糖(Vue 3.4+)
const model = defineModel();
</script>defineModel(Vue 3.4+)大幅简化 v-model:
<!-- 旧写法 -->
<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
</script>
<!-- 新写法:一行搞定 -->
<script setup>
const model = defineModel();
// 直接 model.value = x 就会同步给父组件
</script>二十四、选项式到组合式的实战重构
同一个组件用两种风格对比,直观感受差异。
<!-- Options API 写法 -->
<script>
export default {
data() {
return { count: 0, list: [] };
},
computed: {
double() { return this.count * 2; }
},
watch: {
count(val) { console.log('count 变了', val); }
},
methods: {
increment() { this.count++; },
async fetchList() {
this.list = await api.getList();
}
},
mounted() {
this.fetchList();
}
};
</script><!-- Composition API 写法 -->
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
const count = ref(0);
const list = ref([]);
const double = computed(() => count.value * 2);
watch(count, (val) => console.log('count 变了', val));
function increment() {
count.value++;
}
async function fetchList() {
list.value = await api.getList();
}
onMounted(fetchList);
</script>两种风格的取舍:
| 维度 | Options API | Composition API |
|------|-------------|-----------------|
| 上手难度 | 低(结构固定) | 稍高(需理解响应式) |
| 逻辑组织 | 按选项类型分散 | 按功能聚合 |
| 逻辑复用 | mixins(有缺陷) | composables(优雅) |
| 大型组件 | 逻辑跳来跳去 | 相关逻辑集中 |
| TypeScript | 支持一般 | 支持优秀 |
| this 指向 | 需注意 | 无 this 困扰 |
建议: 小型简单组件用哪种都行;中大型组件、需要复用逻辑、重度使用 TS 的场景,优先 Composition API。Vue3 两种都支持,可渐进采用。
二十五、迁移指南:从 Vue2 到 Vue3
1. 破坏性变更清单(重点)
| 类别 | Vue2 | Vue3 变化 |
|------|------|-----------|
| 初始化 | new Vue() | createApp() |
| 全局 API | Vue.xxx | app.xxx / 按需导入 |
| v-model | value/input | modelValue/update:modelValue |
| .sync | 支持 | 移除,用多 v-model |
| filters | 支持 | 移除 |
| $on/$off/$once | 支持 | 移除 |
| $children | 支持 | 移除,用 ref/$refs |
| 函数式组件 | functional: true | 普通函数组件 |
| 异步组件 | 工厂函数 | defineAsyncComponent |
| 自定义指令 | bind/inserted... | mounted/updated... |
| v-if/v-for 优先级 | v-for 高 | v-if 高 |
| 事件默认值 | key modifier 宽松 | 更严格 |
2. 使用迁移构建版(@vue/compat)
对大型 Vue2 项目,官方提供了兼容版本,让项目先跑在"Vue3 + 兼容模式"下,逐步消除警告再切换。
// 用 @vue/compat 替换 vue,兼容大部分 Vue2 写法并给出迁移警告
// vite.config.js / vue.config.js 中做别名
resolve: {
alias: {
vue: '@vue/compat'
}
}// 配置兼容级别
import { configureCompat } from 'vue';
configureCompat({
MODE: 2 // 2=尽量兼容 Vue2;逐个特性可单独设为 3
});3. 迁移步骤建议
4. 生态迁移对照
| 库 | Vue2 | Vue3 |
|----|------|------|
| 路由 | vue-router 3 | vue-router 4 |
| 状态 | vuex 3 | pinia(或 vuex 4) |
| UI(Element) | element-ui | element-plus |
| UI(Ant) | ant-design-vue 1 | ant-design-vue 3+ |
| 构建 | vue-cli(webpack) | create-vue(Vite) |
| 工具库 | 各种 mixin | VueUse(composables) |
二十六、常见迁移坑
坑 1:reactive 解构丢响应式
const state = reactive({ count: 0 });
const { count } = state; // count 不再响应式,用 toRefs坑 2:ref 忘记 .value
在 script 中访问 ref 必须加 .value,模板中才自动解包。这是最高频的新手错误。
坑 3:EventBus 失效
Vue3 移除了 $on/$off,老项目里的 EventBus 会直接报错,需换成 mitt 或状态管理。
坑 4:v-model 不生效
自定义组件的 v-model prop 从 value 变成 modelValue,事件从 input 变成 update:modelValue,迁移时容易漏改。
坑 5:过滤器报错
模板里的 | filter 语法在 Vue3 会编译报错,需全部改为方法调用。
坑 6:生命周期钩子改名
beforeDestroy/destroyed 在 Vue3 仍可用但会警告,应改为 beforeUnmount/unmounted。
坑 7:多根组件属性透传警告
Vue3 多根组件的 $attrs 不会自动透传,需手动 v-bind="$attrs" 或设 inheritAttrs。
二十七、面试高频问答
Q1:Vue3 为什么用 Proxy 替代 Object.defineProperty?
Object.defineProperty 无法监听对象属性的新增/删除、无法监听数组索引和 length 变化,需要 Vue.set/Vue.delete 补救;Proxy 能拦截整个对象的所有操作(含新增删除、数组变化),从根本上消除了响应式盲区,且是惰性代理(访问到才递归),初始化性能更好。
Q2:Composition API 解决了什么问题?
解决了 Options API 在大型组件中"同一功能的逻辑被拆散到 data/methods/computed/watch 各处"的问题,让相关逻辑能聚合在一起;同时用 composable 优雅替代 mixins,解决了 mixins 的命名冲突、数据来源不清、类型不友好等缺陷。
Q3:ref 和 reactive 怎么选?
基本类型只能用 ref;对象两者都可以。实践中常见做法是:简单值用 ref,一组相关状态用 reactive,或统一用 ref 保持一致性。注意 reactive 解构会丢响应式、不能整体替换。
Q4:Vue3 性能提升来自哪里?
主要来自编译时优化:静态提升(静态节点只创建一次)、PatchFlag(只 diff 动态部分)、Block Tree(跳过静态层级扁平化 diff);此外 Proxy 惰性响应式、Tree Shaking 减小体积、SSR 优化也有贡献。
Q5:script setup 相比 setup() 有什么优势?
更简洁(无需 return)、性能更好(编译时生成更优代码)、更好的 TypeScript 支持、模板中可直接使用顶层绑定;配合 defineProps/defineEmits/defineModel 等编译宏写起来非常顺手。
Q6:Vue2 项目要不要迁移到 Vue3?
Vue2 已于 2023 年底停止维护,长期看应迁移。小项目可直接重写;大项目用 @vue/compat 渐进迁移。若短期没有新需求且运行稳定,可评估成本后择机进行,但不宜长期停留在无维护的版本上。
Q7:Vue3 还支持 Options API 吗?
支持。Vue3 完全兼容 Options API,Options 和 Composition 可以在项目中共存甚至同一组件混用。这保证了 Vue2 用户平滑过渡,也让简单组件仍可用熟悉的写法。
总结
Vue3 不是对 Vue2 的小修小补,而是一次面向未来的架构级重构:Proxy 响应式解决了检测盲区,Composition API 解决了大型组件的逻辑组织与复用,编译时优化和 Tree Shaking 带来了性能与体积的双重飞跃,一流的 TypeScript 支持则让大型工程更可靠。
| 维度 | Vue2 | Vue3 |
|------|------|------|
| 性能 | 较好 | 更优(渲染快 1.3-2 倍) |
| 体积 | 约 30KB | 约 10KB,支持 Tree Shaking |
| API | Options API | Composition API + Options API |
| 响应式 | Object.defineProperty | Proxy |
| 响应式盲区 | 新增/删除属性、数组索引 | 全部支持 |
| TypeScript | 支持一般 | 支持优秀 |
| 逻辑复用 | mixins | composables |
| 多根节点 | 不支持 | 支持(Fragment) |
| 状态管理 | Vuex 3 | Pinia |
| 路由 | Vue Router 3 | Vue Router 4 |
| 内置组件 | 基础 | 新增 Teleport / Suspense |
| 构建工具 | webpack 为主 | Vite 为主 |
| 维护状态 | 已停止维护 | 官方主力 |
一句话总结:新项目无脑选 Vue3,老项目按规模选择渐进迁移策略,理解两者差异的核心在于 Proxy 与 Composition API 这两大变革。