想知道 Vue 3 与 Vue 2 的区别?整理版上手教程
原文从选项式 API、组合式 API、响应式、生命周期、watch/computed、组件通信、
v-model和路由几个方向比较 Vue 2 与 Vue 3。本文尽量保留原来的章节和示例,但把损坏的代码块、Vue 2/3 API 混用和不准确的结论整理为 Vue 2.7 与 Vue 3.5 都能对照理解的版本。
Vue 2.7 仍然适合维护历史项目,Vue 3.5 是新项目的主流版本。升级时不只是把 this 改成 ref,还要同时检查构建工具、插件、Router、状态管理、组件库和 SSR 框架的兼容性。
一、选项式 API 与组合式 API
两种 API 都可以完成同样的功能,主要区别在于组织代码的方式:
- 选项式 API:按
data、methods、computed、watch和生命周期等选项组织; - 组合式 API:按一个功能所需的状态、计算值、副作用和方法组织;
<script setup>:组合式 API 的编译时语法糖,顶层绑定会自动暴露给同一组件的 template。
选项式 API 不是被 Vue 3 删除的旧 API,Vue 3 仍然支持它;组合式 API 也不是所有组件都必须使用的“唯一正确答案”。大型组件中组合式 API 更容易按功能复用和拆分,小型组件使用选项式 API 也可以很清晰。
1. 选项式 API
<template>
<button @click="changeMsg">{{ msg }}</button>
</template>
<script>
export default {
data() {
return {
msg: 'hello world',
}
},
methods: {
changeMsg() {
this.msg = 'hello Vue'
},
},
}
</script>
2. 组合式 API
<template>
<button @click="changeMsg">{{ msg }}</button>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const msg = ref('hello world')
function changeMsg() {
msg.value = 'hello Vue'
}
return { msg, changeMsg }
},
})
</script>
3. <script setup>
<template>
<button @click="changeMsg">{{ msg }}</button>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('hello world')
function changeMsg() {
msg.value = 'hello Vue'
}
</script>
在普通 setup() 中,返回值才会暴露给 template;在 <script setup> 中,顶层声明由编译器自动暴露,不需要手写 return。组件、directive 和普通 import 也可以直接在模板中使用。
组合式 API 的优势不是“少写几行代码”,而是可以把相关逻辑放在一起,并通过 composable 复用:
// useCounter.ts
import { computed, ref } from 'vue'
export function useCounter() {
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
return { count, double, increment }
}
二、ref 和 reactive
Vue 2 中,data() 返回的对象会被 Vue 转换为响应式数据;Vue 3 的组合式 API 可以使用 ref() 和 reactive() 显式创建响应式状态。
<script setup>
import { reactive, ref } from 'vue'
const msg = ref('hello world')
const user = reactive({
name: 'Vue',
age: 3,
})
function changeData() {
msg.value = 'hello Vue'
user.name = 'Vue 3'
}
</script>
ref
ref 可以保存基本值,也可以保存对象、数组:
const count = ref(0)
count.value++
const list = ref([{ id: 1 }])
list.value.push({ id: 2 })
在 JavaScript 中访问或修改 ref 通常需要 .value;在 Vue 模板中会自动解包顶层 ref:
<template>
<button @click="count++">{{ count }}</button>
</template>
reactive
reactive 通常用于对象、数组和 Map/Set:
const state = reactive({
message: 'hello',
count: 0,
})
state.count++
reactive 有几个边界:
- 只能接收对象、数组或集合,不能直接代理基本值;
- 返回的是 Proxy,和原对象的
===身份不同; - 不应把整个 reactive 变量替换成新对象,否则原有依赖仍指向旧代理;
- 直接解构属性会失去该属性的响应式连接,可以使用
toRef/toRefs; - 第三方实例或外部代理状态有时适合
shallowRef、shallowReactive或markRaw。
import { reactive, toRefs } from 'vue'
const state = reactive({ message: 'Vue', count: 0 })
const { message, count } = toRefs(state)
“基本类型用 ref、复杂类型用 reactive”是便于入门的经验,不是硬性规则。ref 同样可以包装复杂对象,而且在 composable 返回状态时往往更灵活。
三、生命周期
Vue 3 的 Composition API 使用 onMounted 等 hook 注册生命周期;Vue 3 的 Options API 仍然支持 beforeCreate、created 等选项。
| Vue 2 Options API | Vue 3 Options API | Vue 3 Composition API | 说明 |
|---|---|---|---|
beforeCreate | beforeCreate | 无直接同名 hook | setup() 在组件创建早期执行 |
created | created | 无直接同名 hook | setup() 通常承接其中的状态初始化逻辑 |
beforeMount | beforeMount | onBeforeMount | 首次 DOM 挂载前 |
mounted | mounted | onMounted | 组件 DOM 挂载后 |
beforeUpdate | beforeUpdate | onBeforeUpdate | DOM 更新提交前 |
updated | updated | onUpdated | DOM 更新后 |
beforeDestroy | beforeUnmount | onBeforeUnmount | 卸载前 |
destroyed | unmounted | onUnmounted | 卸载完成 |
activated | activated | onActivated | KeepAlive 激活 |
deactivated | deactivated | onDeactivated | KeepAlive 停用 |
errorCaptured | errorCaptured | onErrorCaptured | 捕获后代错误 |
Vue 3 还提供 onServerPrefetch、onRenderTracked 和 onRenderTriggered 等 hook。SSR 中没有浏览器 DOM,因此 onMounted 不会在服务端执行。
onMounted 示例
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('组件 DOM 已挂载')
})
</script>
setup() 不是一个“新的 created 钩子”,而是组件创建阶段执行的入口。Vue 3 中没有对应的 onBeforeCreate/onCreated Composition API hook,因为 setup 本身已经覆盖了这段初始化时机;如果使用 Options API,原有两个选项仍可使用。
四、watch、computed 和 watchEffect
1. computed:计算派生状态
<script>
export default {
data: () => ({ a: 1, b: 2 }),
computed: {
sum() {
return this.a + this.b
},
},
}
</script>
Composition API:
import { computed, ref } from 'vue'
const a = ref(1)
const b = ref(2)
const sum = computed(() => a.value + b.value)
computed 默认惰性求值并缓存结果,只有依赖变化后才会重新计算。getter 应保持纯净,不要在其中发请求、修改其他状态或操作 DOM。
2. watch:监听明确数据源并执行副作用
import { ref, watch } from 'vue'
const a = ref(1)
const stop = watch(a, (newValue, oldValue) => {
console.log(`${oldValue} -> ${newValue}`)
})
// 不需要时可以停止
// stop()
watch 的 source 可以是 ref、reactive 对象、getter 或 source 数组:
watch(
() => route.params.id,
id => loadArticle(String(id)),
{ immediate: true },
)
watch 默认不会立即执行;immediate: true 才会在注册时先执行一次。deep、flush、once 等选项要根据场景使用,深度监听大型对象可能有明显成本。
3. watchEffect:自动追踪同步读取的依赖
<script setup>
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
const watchTarget = ref(0)
let timer
const stop = watchEffect(() => {
console.log(watchTarget.value)
})
onMounted(() => {
timer = window.setInterval(() => {
watchTarget.value++
}, 1000)
})
onUnmounted(() => {
if (timer) window.clearInterval(timer)
stop()
})
</script>
watchEffect 会立即执行一次,并追踪回调中同步读取的响应式依赖;依赖变化后重新执行。它与 React useEffect 有相似之处,但调度、依赖收集和清理 API 不完全相同,也不需要依赖数组。异步回调中,首次 await 之后读取的依赖不会按同样方式自动追踪。
Vue 3.5 可以用 onWatcherCleanup 取消过期请求;较早版本也可以使用 watch 回调的第三个 onCleanup 参数:
import { onWatcherCleanup, watch } from 'vue'
watch(id, async value => {
const controller = new AbortController()
onWatcherCleanup(() => controller.abort())
const response = await fetch(`/api/${value}`, {
signal: controller.signal,
})
data.value = await response.json()
})
选择原则:
- 要得到一个派生值:
computed; - 要监听明确数据源并做副作用:
watch; - 要自动追踪当前回调读取的依赖:
watchEffect。
五、组件通信
| 场景 | Vue 2 常见方式 | Vue 3 推荐方式 |
|---|---|---|
| 父传子 | props | props |
| 子传父 | $emit | defineEmits/emit |
| 多层传递 | provide/inject | provide/inject |
| 组件透传 | $attrs、$listeners | $attrs,未声明监听器也在其中 |
| 父访问子 | $refs | template ref + defineExpose |
| 兄弟/跨模块 | EventBus、共同父状态 | 共同父状态、composable、Pinia、明确事件通道 |
1. props:父向子传递
父组件:
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentMsg = ref('父组件信息')
</script>
<template>
<Child :msg="parentMsg" />
</template>
子组件:
<script setup>
const props = defineProps<{
msg: string
}>()
</script>
<template>
<p>{{ props.msg }}</p>
</template>
props 是单向下行绑定,子组件不应直接修改 prop。若需要编辑,应 emit 一个事件让父组件更新;若只是从 prop 派生本地值,可以复制到 ref,或用 computed 计算。
在 Vue 3.5 中,响应式 props destructure 已有编译器支持;为了兼容较早的 Vue 3 版本,仍可显式使用 toRef/toRefs:
const props = defineProps<{ msg: string }>()
const msg = toRef(props, 'msg')
2. emits:子向父通知
子组件:
<script setup lang="ts">
const emit = defineEmits<{
sendMsg: [value: string]
}>()
function send() {
emit('sendMsg', '我是子组件数据')
}
</script>
<template>
<button @click="send">发送</button>
</template>
父组件:
<Child @send-msg="value => console.log(value)" />
Options API 中仍可以使用:
export default {
emits: ['sendMsg'],
methods: {
send() {
this.$emit('sendMsg', '我是子组件数据')
},
},
}
3. $attrs 和 $listeners
Vue 2 中,$attrs 通常表示没有被 props 声明的属性,$listeners 表示父组件传入的监听器。Vue 3 移除了 $listeners,未被 emits 声明的监听器会合并到 $attrs;class 和 style 也属于 fallthrough attrs 的处理范围。
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
<template>
<!-- 将未声明的属性和监听器透传给真正的 input -->
<input v-bind="attrs">
</template>
如果一个组件声明了 emits: ['sendMsg'],父组件的 @send-msg 就会被视为组件事件,而不是普通 $attrs。不建议通过 attrs.onParentFun() 主动调用父组件传来的函数,这会绕过 props/emits 的契约;需要子组件通知父组件时,应声明并 emit 事件。
需要控制透传目标时可以使用 inheritAttrs: false(Options API)或 defineOptions({ inheritAttrs: false })(Vue 3.3+),再显式 v-bind="$attrs"。
4. provide/inject:跨层级依赖
适合主题、表单上下文、服务对象等跨层级依赖,不要把所有全局业务状态都藏进注入对象。
推荐使用 Symbol key:
// keys.ts
import type { InjectionKey, Ref } from 'vue'
export const messageKey: InjectionKey<Ref<string>> = Symbol('message')
提供方:
<script setup lang="ts">
import { provide, ref } from 'vue'
import { messageKey } from './keys'
const message = ref('父组件信息')
provide(messageKey, message)
</script>
后代组件:
<script setup lang="ts">
import { inject } from 'vue'
import { messageKey } from './keys'
const message = inject(messageKey)
</script>
<template>
<p>{{ message }}</p>
</template>
注入一个 ref 时,ref 本身会保留响应式连接;Options API 中直接 provide() { return { msg: this.msg } } 传递普通字符串通常只是提供当时的值。需要响应式更新时,应提供 ref、reactive 对象或明确的方法。
5. $parent、$children 和 template ref
Vue 2 的 $parent 可以访问父实例,$children 是直接子组件数组,但它不是响应式集合,也不应依赖其顺序。Vue 3 移除了 $children;$parent 仍可能在公共实例上存在,但强耦合、难测试,不推荐作为常规通信方式。
父组件访问子组件的命令式能力应使用 template ref,并且只暴露必要 API:
<!-- Parent.vue -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'
const child = useTemplateRef('child')
onMounted(() => {
child.value?.focusInput()
})
</script>
<template>
<Child ref="child" />
</template>
useTemplateRef 是 Vue 3.5 的便利 API;较早版本可以使用 const child = ref(null) 并让模板 ref 使用同名变量。
<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const input = ref(null)
function focusInput() {
input.value?.focus()
}
defineExpose({ focusInput })
</script>
<template>
<input ref="input">
</template>
<script setup> 组件默认不会把所有顶层绑定作为公共实例 API 暴露给父组件,使用 defineExpose 可以明确控制边界。模板 ref 只有在挂载后才有 DOM/组件实例值,卸载后可能恢复为 null。
6. EventBus 和 mitt
Vue 2 可以通过一个 Vue 实例的 $on/$emit/$off 做 EventBus;Vue 3 移除了组件实例事件 API,因此不能继续 new Vue() 作为总线。mitt 等第三方库仍然可以实现发布订阅,但它不是 Vue 内置替代品:
// bus.ts
import mitt from 'mitt'
export const bus = mitt<{
'message': string
}>()
import { onUnmounted } from 'vue'
import { bus } from './bus'
function handleMessage(message: string) {
console.log(message)
}
bus.on('message', handleMessage)
onUnmounted(() => {
bus.off('message', handleMessage)
})
事件总线容易产生隐式依赖和忘记取消订阅的问题。兄弟组件优先提升状态到共同父组件;跨页面或业务共享状态优先考虑 Pinia;确实需要事件通道时,必须约定事件名、类型和生命周期。
六、v-model 和 .sync
1. Vue 2 的组件 v-model
Vue 2 默认把组件上的:
<Child v-model="pageTitle" />
展开为:
<Child
:value="pageTitle"
@input="pageTitle = $event"
/>
也可以通过组件 model 选项改用其他 prop/event。Vue 2 的 .sync 常见写法是:
<Child :title.sync="pageTitle" />
它对应:
<Child
:title="pageTitle"
@update:title="pageTitle = $event"
/>
2. Vue 3 的组件 v-model
Vue 3 默认契约变为:
<Child v-model="pageTitle" />
等价于:
<Child
:model-value="pageTitle"
@update:model-value="pageTitle = $event"
/>
子组件可以这样实现:
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function onInput(event: Event) {
emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>
<template>
<input
:value="props.modelValue"
@input="onInput"
>
</template>
3. 参数、多个模型和 defineModel
.sync 在 Vue 3 被移除,使用带参数的 v-model:
<Child v-model:title="title" v-model:content="content" />
它分别对应 title/update:title 和 content/update:content。Vue 3.4+ 可以使用 defineModel 简化组件:
<script setup lang="ts">
const title = defineModel<string>('title', { required: true })
</script>
<template>
<input v-model="title">
</template>
因此,v-model 并不是任意状态自动互相修改,而是 prop + update 事件的约定;组件仍保持清晰的父子数据流。
七、路由相关写法
Vue Router 3 和 Vue Router 4/5 的核心概念相近,但创建路由、守卫返回值和 Composition API 写法不同。
1. Options API 中访问路由
<script>
export default {
methods: {
async toPage() {
await this.$router.push({ name: 'home' })
},
},
created() {
console.log(this.$route.params)
console.log(this.$route.query)
},
beforeRouteLeave(to, from) {
if (this.hasUnsavedChange) return false
},
}
</script>
Vue Router 3 常见的组件守卫使用 next:
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChange) {
next(false)
return
}
next()
}
在 Vue Router 4/5 中,推荐返回 true、false、重定向位置或抛出错误,不要在同一个守卫中混用返回值和 next。
2. Composition API 中访问路由
<script setup lang="ts">
import {
onBeforeRouteLeave,
onBeforeRouteUpdate,
useRoute,
useRouter,
} from 'vue-router'
import { watch } from 'vue'
const router = useRouter()
const route = useRoute()
function toPage() {
router.push({ name: 'home' })
}
watch(
() => route.params.id,
id => loadData(String(id)),
{ immediate: true },
)
onBeforeRouteUpdate(to => {
console.log('参数变化:', to.params.id)
return true
})
onBeforeRouteLeave(() => {
if (hasUnsavedChange()) return false
return true
})
</script>
useRoute() 返回响应式 route 对象;不要把整个 route 对象作为一个大 watch source,通常只监听需要的 route.params.id 或 route.query.keyword。
beforeRouteEnter 是 Options API 组件守卫,因为它发生在组件实例创建前,所以没有对应的 onBeforeRouteEnter Composition API hook。需要在进入前校验可以使用路由记录的 beforeEnter、全局守卫或数据加载方案;进入组件后再根据参数请求数据则可以在 setup、watch 或 onBeforeRouteUpdate 中完成。
3. Router 4/5 与 Router 3 的创建差异
Vue Router 3:
const router = new VueRouter({
mode: 'history',
routes,
})
Vue Router 4/5:
import {
createRouter,
createWebHistory,
} from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes,
})
Router 4/5 使用 router.addRoute() 动态添加一条路由,Router 3 历史项目中常见 addRoutes()。History 模式部署时需要服务器 fallback,Hash 模式使用 createWebHashHistory()。
4. Nuxt 4 的补充
Nuxt 4 采用文件系统路由,页面通常放在 app/pages/,导航可以使用 Nuxt 的 navigateTo 或自动导入的路由组件。只有在明确使用 Vue Router 底层 API 时,才需要在 Nuxt 语境中手动创建 router;不要把纯 Vite Vue 项目的 main.ts 路由初始化代码原样复制到 Nuxt 项目。
八、从 Vue 2 迁移到 Vue 3 的检查清单
- 构建与入口:从
new Vue()迁移到createApp();检查 Vite、Webpack、插件和vue-loader; - 响应式:识别
$set/$delete、数组特殊写法和直接解构响应式对象的代码; - 生命周期:
beforeDestroy/destroyed改为beforeUnmount/unmounted; - 组件通信:移除
$listeners、$children和组件实例 EventBus;补充 emits、defineExpose; - v-model:
value/input改为modelValue/update:modelValue,.sync改为带参数的v-model; - 路由:Router 3 的
mode、next和addRoutes对照 Router 4/5 的 API 迁移; - 状态管理:Vuex 4 可继续维护,新的 Vue 3 项目通常优先 Pinia;
- 第三方库:检查 UI 组件、指令、过滤器、过渡、SSR 和测试工具的 Vue 3 版本;
- 模板行为:检查 Fragment、多根节点、
v-if/v-for优先级、slot 和$attrs变化; - SSR/部署:检查 hydration 一致性、请求级状态隔离和 History fallback。
总结
- 选项式 API 和组合式 API 都是 Vue 的正式能力,区别主要在逻辑组织方式;
ref和reactive都可以创建响应式状态,选择要结合替换、解构、返回 composable 和类型体验;- Vue 3 没有移除 Options API,只是 Composition API 使用 hook 注册生命周期和响应式逻辑;
computed负责缓存的派生值,watch和watchEffect负责副作用;- Vue 3 通过 props/emits、provide/inject、
$attrs、template ref 和 Pinia 组织通信; .sync已由参数化v-model替代,Vue 3.4+ 还可以使用defineModel;- 路由组合式 API 使用
useRoute/useRouter和onBeforeRouteLeave/onBeforeRouteUpdate,beforeRouteEnter仍属于 Options API 语境; - Vue 2.7 和 Vue 3.5 可以并行维护,但 API、构建、插件和生态版本必须一起检查。
参考资料
- Vue 3 组合式 API 基础
- Vue 3 响应式基础
- Vue 3 生命周期
- Vue 3 Watchers
- Vue 3 组件 props
- Vue 3 组件事件
- Vue 3 Fallthrough Attributes
- Vue 3 依赖注入
- Vue 3 组件
v-model - Vue Router 4/5 Composition API
- Vue Router 4/5 导航守卫
- Vue 2.7 迁移说明
- Vue 3 迁移指南
- Pinia 核心概念
原文作者:东方小月。原文关于 Options API、Composition API、ref/reactive、生命周期、watch/computed、组件通信、v-model 和路由的章节予以保留;格式损坏、错误的 Vue 3 生命周期结论、$attrs/$listeners 混淆、错误的 emits 写法、路由守卫 API 混用和推广噪声已清理或修正。