Vue 2 到 Vue 3 详细衔接教程(小白也能看懂)
如果你学过 Vue 2,现在想升级到 Vue 3——或者你看到的教程有 Vue 2 有 Vue 3,分不清该学哪个——这一篇就是为你准备的。
Vue 3 是 Vue 官方于 2020 年发布的重大版本,现在是 Vue 的绝对主流,新项目几乎全用 Vue 3。但 Vue 3 的写法变化很大,很多 Vue 2 老手都会绕晕。这一篇用逐条对比 + 可运行示例,带你把 Vue 2 和 Vue 3 的差异全部理清楚。
提醒:如果你还没学过 Vue,建议先看前面那篇《Vue / React 框架入门》,再回来看这篇衔接教程。
一、Vue 3 到底改了什么?一张总览图
Vue 2 → Vue 3 的变化主要分四块:
| 层面 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式原理 | Object.defineProperty(有局限) | Proxy(更强大) |
| 组合代码 | Options API(唯一) | Composition API(新增)+ Options API 共存 |
| 性能 | 较好 | 更快、更小(Tree-shaking) |
| 生态 | Vuex、Vue Router 3 | Pinia、Vue Router 4 |
| 原生支持 | 不支持 TS | 原生 TS 支持 |
核心变化一句话: Vue 3 用更现代的 Proxy 重写了底层,同时新增了 Composition API(组合式)这种更灵活的写法,但 Options API(选项式)在 Vue 3 里依然能用——这是很多人容易忽略的关键点。
最让你安心的消息: 你会的 Vue 2 写法,在 Vue 3 里大部分依然能跑。Vue 3 并没有推翻一切,而是”加新 + 优化”。
二、Composition API vs Options API:两种写法
这是 Vue 2 → Vue 3 最核心的区别。先看懂这两种 API。
Options API:Vue 2 的传统写法
Vue 2 只能这样写——把代码按”选项”(data、methods、computed…)分类:
<template> <button @click="add">点击 {{ count }}</button></template>
<script>export default { // data 选项:数据 data() { return { count: 0 }; }, // methods 选项:方法 methods: { add() { this.count++; } }, // computed 选项:计算属性 computed: { double() { return this.count * 2; } }, // watch 选项:监听 watch: { count(newVal) { console.log("count 变了:", newVal); } }};</script>特点:按”功能类型”分块(数据放一起、方法放一起)。优点是有规矩;缺点是当一个组件功能多时,相关代码被拆散在不同选项里,不好维护。
Composition API:Vue 3 新增写法
把”同一个功能相关的代码”放在一起:
<template> <button @click="add">点击 {{ count }}</button></template>
<script setup>import { ref, computed, watch } from "vue";
// 所有和 count 相关的代码都在一起const count = ref(0);
function add() { count.value++;}
const double = computed(() => count.value * 2);
watch(count, (newVal) => { console.log("count 变了:", newVal);});</script>特点:按”业务逻辑”组织,相关代码聚在一起,更容易维护和复用。而且 <script setup> 语法让代码更简洁——你不用写 export default,也不用 return。
两种写法怎么选?
| Options API | Composition API | |
|---|---|---|
| 适合 | 简单组件、Vue 2 老手过渡 | 复杂组件、需要复用逻辑 |
| 学习成本 | 低 | 略高 |
| 代码组织 | 按类型分块 | 按功能聚合 |
| 复用逻辑 | 用 mixin(有坑) | 用组合式函数(推荐) |
官方建议: Vue 3 新项目首选 Composition API(<script setup>)。但 Options API 依然合法,你想用哪个都行。
三、响应式数据:data → ref / reactive
这是最容易混淆的地方。
Vue 2 的 data
export default { data() { return { count: 0, user: { name: "小明", age: 18 } }; }};Vue 3 的 ref:单个值
ref 用来包装单个基本值(数字、字符串、布尔):
<script setup>import { ref } from "vue";
const count = ref(0);const name = ref("小明");
// 模板里直接用(自动解开)// JS 代码里要 .valuefunction add() { count.value++;}</script>
<template> <p>{{ count }} - {{ name }}</p></template>关键坑: 模板里 count 直接用,但 JS 里必须写 count.value。
Vue 3 的 reactive:对象
reactive 用来包装对象:
<script setup>import { reactive } from "vue";
const user = reactive({ name: "小明", age: 18});
// 对象属性不用 .value,直接改function birthday() { user.age++;}</script>
<template> <p>{{ user.name }} - {{ user.age }}</p></template>ref vs reactive 怎么选?
一个简单建议: 基本类型和简单的值用 ref;对象用 reactive。但要注意——ref 也可以包对象,而且现在的写法越来越倾向于统一用 ref(更一致)。
| ref | reactive | |
|---|---|---|
| 包什么 | 基本值 / 对象都可以 | 只能包对象 |
| JS 里访问 | 要 .value | 直接 .属性 |
| 模板里 | 自动解开 | 直接用 |
小技巧: 不确定用哪个就都用 ref,出错少。
const user = ref({ name: "小明" });console.log(user.value.name); // 小明四、computed 和 watch:写法变化
computed(计算属性)
Vue 2:
computed: { double() { return this.count * 2; }}// 使用:this.doubleVue 3(Composition):
<script setup>import { ref, computed } from "vue";
const count = ref(2);const double = computed(() => count.value * 2);// 模板里直接用 double</script>watch(监听)
Vue 2:
watch: { count(newVal, oldVal) { console.log("变了", newVal, oldVal); }}Vue 3(Composition):
<script setup>import { ref, watch } from "vue";
const count = ref(0);watch(count, (newVal, oldVal) => { console.log("变了", newVal, oldVal);});注意: Vue 3 的 watch 第一个参数是 ref 对象(不用写 .value),第二个是回调。
监听对象属性要这样:
<script setup>import { reactive, watch } from "vue";
const user = reactive({ name: "小明" });watch(() => user.name, (newVal) => { console.log("名字变了:", newVal);});</script>五、生命周期:改名 + 变成函数
Vue 的生命周期(组件从创建到销毁的过程)在 Vue 3 里基本同名,但 Options 写法能对应,Composition 写法变成函数了。
对比表
| Vue 2 (Options) | Vue 3 (Options) | Vue 3 (Composition) |
|---|---|---|
| beforeCreate | beforeCreate | (不需要,直接写在 setup) |
| created | created | (同上) |
| beforeMount | beforeMount | onBeforeMount |
| mounted | mounted | onMounted |
| beforeUpdate | beforeUpdate | onBeforeUpdate |
| updated | updated | onUpdated |
| beforeDestroy | beforeUnmount | onBeforeUnmount |
| destroyed | unmounted | onUnmounted |
两个改名: beforeDestroy → beforeUnmount,destroyed → unmounted。
Vue 2 写法
export default { created() { console.log("组件创建"); }, mounted() { console.log("已挂载"); }, destroyed() { console.log("销毁了"); }};Vue 3 Composition 写法
<script setup>import { onMounted, onUnmounted } from "vue";
// created/mounted 之前的内容直接写在这里console.log("组件创建");
onMounted(() => { console.log("已挂载");});
onUnmounted(() => { console.log("销毁了");});</script>关键: 在 <script setup> 里,没有 created/beforeCreate 对应的函数——因为 setup 本身就发生在它们之前,直接在 setup 里写代码就相当于 created 时执行。
六、v-model:绑定语法变化
Vue 2 的 v-model
<!-- 相当于 :value + @input --><input v-model="name">Vue 2 里 v-model 默认用 value 属性和 input 事件。
Vue 3 的 v-model:更灵活
Vue 3 里 v-model 默认改用 modelValue 属性和 update:modelValue 事件:
<!-- Vue 3 单个 --><input v-model="name">最重要的新特性:一个组件可以用多个 v-model! Vue 2 只能一个。
<my-form v-model="name" v-model:email="email" />自定义组件里怎么实现 v-model
Vue 2:
export default { props: ["value"], methods: { update() { this.$emit("input", newVal); } }};Vue 3:
<script setup>defineProps(["modelValue"]);const emit = defineEmits(["update:modelValue"]);// 触发更新:emit("update:modelValue", newVal)</script>七、过滤器 filters:被移除了!
Vue 2 有 filters(过滤器),用来格式化文本:
<!-- Vue 2 --><p>{{ price | currency }}</p>// Vue 2filters: { currency(v) { return "¥" + v; }}Vue 3 移除了 filters,官方建议用方法或计算属性代替:
<!-- Vue 3:改用方法 --><p>{{ currency(price) }}</p><script setup>function currency(v) { return "¥" + v;}</script>如果你从 Vue 2 迁移,记得把所有 filters 改成方法或 computed。
八、全局 API 变化:Vue.xxx 改成了导入
Vue 2 里很多全局 API 挂载在 Vue 上:
// Vue 2Vue.component("my-comp", { ... }); // 注册全局组件Vue.directive("focus", { ... }); // 注册全局指令Vue.mixin({ ... }); // 全局混入Vue 3 里这些改成了从 vue 导入的独立函数:
// Vue 3import { createApp } from "vue";
const app = createApp(App);app.component("my-comp", { ... });app.directive("focus", { ... });app.mixin({ ... });
app.mount("#app");最大的变化是 new Vue() → createApp():
// Vue 2new Vue({ render: h => h(App) }).$mount("#app");
// Vue 3import { createApp } from "vue";createApp(App).mount("#app");九、事件 API:off 被移除
Vue 2 里可以用 $on、$emit、$off 做组件间通信(事件总线 EventBus):
// Vue 2 事件总线Vue.prototype.$bus = new Vue();this.$bus.$on("event", callback);this.$bus.$emit("event", data);Vue 3 移除了 $on、$off、$once($emit 还在)。官方推荐替代方案:
- 简单场景用 props / emit
- 复杂场景用 Pinia(状态管理) 或 mitt(轻量事件库)
十、v-for 的 key:规则更严
Vue 2 里 v-for 的 key 建议写但不强制。Vue 3 里 key 是必需的,否则会有警告。
<!-- Vue 3 必须写 key --><li v-for="item in list" :key="item.id"> {{ item.name }}</li>key 用什么? 用能唯一标识该项的值(比如 id),不要用 index(尤其有增删、排序时,用 index 会出 bug)。
十一、样式和模板细节变化
多个根节点
Vue 2 的模板只能有一个根元素(最外层只能包一个 div):
<!-- Vue 2:报错,只能一个根 --><template> <div>...</div></template>Vue 3 支持多个根节点(Fragment 片段):
<!-- Vue 3:可以多个根,不再强制包一层 div --><template> <header>标题</header> <main>内容</main></template>这让模板更干净,少套很多无用 div。
组件事件区分
Vue 3 里,组件上写的 @click 默认是自定义事件,而不是原生 DOM 事件。要监听原生事件需加 .native(Vue 3 已移除 .native,改用在组件内部 emits 声明,或直接用 @click 但组件要透传)。这是迁移时容易踩的坑。
十二、状态管理:Vuex → Pinia
Vue 2 用 Vuex
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: { doIncrement({ commit }) { commit("increment"); } }});Vuex 有 state、mutations、actions、getters 四件套,比较繁琐。
Vue 3 用 Pinia(官方推荐)
Pinia 是 Vue 3 的官方状态管理库,更简单、支持 TS、移除了 mutations(直接用函数修改):
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", { state: () => ({ count: 0 }), actions: { increment() { this.count++; } }});组件里用:
<script setup>import { useCounterStore } from "../store/counter";const store = useCounterStore();</script>
<template> <p>{{ store.count }}</p> <button @click="store.increment">+1</button></template>Pinia 相比 Vuex 的好处: 没有 mutations、getters 更简单、原生支持 Composition API、自动支持 TypeScript。新项目直接用 Pinia。
十三、路由:Vue Router 3 → 4
Vue 2 用 Vue Router 3,Vue 3 用 Vue Router 4。基础用法差不多,几个小变化:
安装
# Vue 3 用 router 4npm install vue-router@4基本配置对比
Vue 2:
import Vue from "vue";import VueRouter from "vue-router";Vue.use(VueRouter);
const router = new VueRouter({ routes: [...]});Vue 3:
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({ history: createWebHistory(), // 用 createWebHistory 代替 mode: "history" routes: [...]});路由守卫变化不大
beforeEach 等守卫基本一样,只是 next() 变成可选(可以直接 return)。
// Vue 3router.beforeEach((to, from) => { if (to.path === "/admin" && !isLogin) { return "/login"; // 直接 return 跳转,不用 next }});十四、从 Vue 2 迁移到 Vue 3 的完整步骤
如果你有个现成的 Vue 2 项目要升级,按这个顺序:
1. 先升级依赖
npm install vue@3npm install vue-router@4npm install pinia # 替代 vuex2. 改入口文件
// main.js(Vue 2 → Vue 3)import { createApp } from "vue";import App from "./App.vue";import router from "./router";import { createPinia } from "pinia";
const app = createApp(App);app.use(router);app.use(createPinia());app.mount("#app");3. 清理 Vue 2 特有写法
filters→ 改成方法/computed$on/$off→ 改用 props/emit 或 PiniaVue.component→app.componentnew Vue()→createApp()
4. 更新生命周期名
beforeDestroy→beforeUnmountdestroyed→unmounted
5. 逐步把 data/methods 改成 ref/composition
不强制一次性全改,可以混合过渡——Options 和 Composition 能共存。
6. 用官方工具检查
Vue 官方提供了 Vue Migration Helper(迁移助手)工具,能自动扫描代码里需要改的地方,强烈推荐用。
十五、一个完整的 Vue 3 项目示例
把学到的串起来,写一个 Vue 3 计数器组件(完整、可运行):
<template> <div class="counter"> <h2>Vue 3 计数器</h2> <p>当前值:{{ count }}</p> <p>翻倍:{{ double }}</p>
<button @click="increment">+1</button> <button @click="decrement">-1</button> <button @click="reset">重置</button>
<p v-if="count > 10" class="tip">你已经超过 10 啦 🎉</p> </div></template>
<script setup>import { ref, computed, watch } from "vue";
// 响应式数据const count = ref(0);
// 方法function increment() { count.value++; }function decrement() { count.value--; }function reset() { count.value = 0; }
// 计算属性const double = computed(() => count.value * 2);
// 监听watch(count, (newVal, oldVal) => { console.log(`count 从 ${oldVal} 变成 ${newVal}`);});</script>
<style scoped>.counter { text-align: center; font-family: sans-serif; }button { margin: 4px; padding: 8px 16px; border: none; border-radius: 6px; background: #42b883; color: white; cursor: pointer;}.tip { color: #e74c3c; }</style>看懂这段就掌握了 Vue 3 核心: ref(数据)、函数(方法)、computed(计算属性)、watch(监听)、v-if(条件)、模板插值。
十六、总结
这一篇我们详细对比了 Vue 2 和 Vue 3:
- 总览:Proxy 重写、Composition API 新增、性能更好、生态更新
- 两种 API:Options(按类型分块)vs Composition(按功能聚合),
<script setup>更简洁 - 响应式:
data→ref/reactive,ref要.value - computed / watch:变成导入的函数
- 生命周期:改名(beforeUnmount/unmounted)+ 变函数
- v-model:支持多个,默认 modelValue
- filter 移除:改用方法/computed
- 全局 API:
new Vue()→createApp() - 事件 API:
$on/$off移除 - v-for key:必填
- 多根节点:Vue 3 支持 Fragment
- Vuex → Pinia:更简单、支持 TS
- Router 3 → 4:createRouter/createWebHistory
- 迁移步骤:改入口、清旧语法、改生命周期、用迁移助手
最后给你一个安心的结论: Vue 3 不是从零重来,而是在 Vue 2 的思路上优化和扩展。你会的 Vue 2 知识没有白学——v-if、v-for、v-model、组件、props、emit 这些核心概念完全一样。你只需要补上 Composition API 和那几处改名/移除的地方,就能平稳过渡到 Vue 3。
下一步: 把上面那个完整计数器示例自己敲一遍跑起来,再把你熟悉的 Vue 2 项目试着用 Vue 3 重写一遍。实践几次,你就完全上手 Vue 3 了。