盘点 Vue 3 与 Vue 2 的区别
原文从生命周期、多根节点、Composition API、Suspense、Teleport、响应式、虚拟 DOM、事件缓存、Diff、Tree-shaking 和 TypeScript 等方面比较 Vue 2 与 Vue 3。本文保留这些主题,修复损坏的 SFC/渲染函数示例,并补充 Vue 2.7、Vue 3.5 和 Nuxt 4 项目中的实际差异。
一、先明确版本和迁移背景
- Vue 2.7 是 Vue 2 的最终次版本,提供了部分 Composition API 能力,但 Vue 2 已结束官方维护;
- Vue 3 是当前主版本,项目通常使用 Vue 3.5.x;
- Vue 2 的
new Vue()、Vue.use()、Vue.set()等写法属于历史 API; - Vue 3 使用
createApp(),生态库通常使用createRouter()、Pinia 和 Composition API; - Vue 3 仍然支持 Options API,Composition API 不是强制替代品;
- Nuxt 4 基于 Vue 3,页面路由、SSR 和数据获取还需要遵循 Nuxt 的约定。
“Vue 3 完全重写所以所有代码都不能用”以及“Vue 3 会淘汰 Options API”都不准确。迁移时应按 API、构建方式和生态依赖逐项检查。
二、生命周期变化
1. 生命周期对照
| Vue 2 Options API | Vue 3 Options API | Vue 3 Composition 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 |
activated | activated | onActivated |
deactivated | deactivated | onDeactivated |
Vue 3 Composition API 的生命周期钩子需要从 vue 导入,并且要在 setup() 执行期间注册:
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
function onResize() {
console.log(window.innerWidth)
}
onMounted(() => {
window.addEventListener('resize', onResize)
})
onUnmounted(() => {
window.removeEventListener('resize', onResize)
})
</script>
同一个钩子可以注册多个回调,它们会按注册顺序执行;不会因为后一次调用就覆盖前一次。
setup() 可以理解为组合式逻辑的入口,而不是简单地把 Vue 2 的 beforeCreate 和 created 两个钩子机械拼接在一起。需要访问真实 DOM 的逻辑仍应放在 onMounted 之后;SSR 中 onMounted 不会在服务端执行。
三、应用实例与全局 API
Vue 2 常见写法:
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
Vue.prototype.$http = http
new Vue({
render: h => h(App),
}).$mount('#app')
Vue 3 改为创建应用实例:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.$http = http
app.provide('http', http)
app.mount('#app')
主要变化:
- Vue 2 的全局配置、插件和原型容易影响同页面的所有根实例;
- Vue 3 的
app实例拥有自己的配置、插件和 provides; - Vue 2
Vue.use(plugin)对应 Vue 3app.use(plugin); - Vue 2
Vue.component()对应 Vue 3app.component(); - Vue 2
Vue.nextTick()在 Vue 3 使用从vue导入的nextTick(); - Vue 3 不再有
Vue.set、Vue.delete,Proxy 可以处理响应式对象的新增/删除。
四、多根节点与 Fragment
Vue 2 的组件模板需要一个根元素:
<!-- Vue 2:通常会报多根节点错误 -->
<template>
<header />
<main />
<footer />
</template>
Vue 3 支持 Fragment,可以有多个根节点:
<template>
<header />
<main />
<footer />
</template>
但多根组件的属性继承需要显式处理。Vue 3 无法猜测 $attrs 应该落到哪一个根节点:
<script setup>
defineOptions({ inheritAttrs: false })
defineProps<{ title: string }>()
</script>
<template>
<header>标题</header>
<main v-bind="$attrs" />
</template>
Vue 3.3+ 可以使用 defineOptions;不使用 <script setup> 时可以写 inheritAttrs: false。Fragment 不是把所有属性自动平均分配给多个根节点。
五、Composition API
Composition API 是一组导入式 API,包含 ref、reactive、computed、watch、生命周期钩子和 provide/inject 等。
Vue 2.7 已内置部分 Composition API,旧版 Vue 2 可使用官方维护过的 @vue/composition-api 插件;Vue 3 则原生支持,并常与 <script setup> 一起使用。
Options API 示例
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
},
},
mounted() {
console.log(this.count)
},
})
</script>
<template>
<button @click="increment">count is: {{ count }}</button>
</template>
Composition API 示例
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
onMounted(() => {
console.log(count.value)
})
</script>
<template>
<button @click="increment">count is: {{ count }}</button>
</template>
Composition API 的优势主要是:
- 按业务逻辑组织代码,而不是把同一逻辑拆散到
data、methods、watch和生命周期中; - 通过 composable 复用有状态逻辑,减少 mixin 的命名冲突和隐式依赖;
- 普通变量和函数更利于 TypeScript 推导;
- 在大型组件和复杂逻辑中更容易抽取、测试和组合。
Options API 仍然是 Vue 3 的正式 API,简单组件或团队更熟悉 Options API 时继续使用完全合理。两种 API 也可以在同一个组件中组合,但应明确状态归属,避免让同一逻辑在两套风格之间来回跳转。
六、异步组件与 Suspense
1. 异步组件
Vue 2 常见异步组件工厂写法:
Vue.component('async-component', function (resolve) {
require(['./MyComponent.vue'], resolve)
})
Vue 3 推荐 defineAsyncComponent 和动态 import():
import { defineAsyncComponent } from 'vue'
const AsyncPanel = defineAsyncComponent({
loader: () => import('./Panel.vue'),
loadingComponent: LoadingPanel,
errorComponent: ErrorPanel,
delay: 200,
timeout: 10000,
})
2. Suspense 的边界
Vue 3 提供内置 <Suspense>,可以等待异步组件或包含顶层 await 的 async setup(),并在 pending 阶段显示 fallback:
<template>
<Suspense>
<template #default>
<AsyncPanel />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
需要注意:
- 当前 Vue 文档仍将 Suspense 标为实验性 API,升级时应关注版本说明;
- 它主要等待异步依赖,不是任意“计算量很大所以自动显示 loading”;
- 请求错误处理、重试和业务空状态仍应由组件或数据层负责;
- 在 Nuxt 4 中还要结合 Nuxt 的 SSR、
useAsyncData/useFetch和页面 loading 方案判断是否需要手写 Suspense。
七、Teleport
Vue 3 的 <Teleport> 可以把一部分 DOM 渲染到组件逻辑之外的目标节点,常用于 Dialog、Popover、全局通知:
<template>
<button @click="visible = true">显示弹窗</button>
<Teleport to="body">
<div v-if="visible" class="dialog">
我在 body 下渲染,但仍属于当前组件的逻辑树
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
</script>
Teleport 改变的是 DOM 挂载位置,不会改变组件的逻辑父子关系:props、事件、provide/inject 仍按组件树工作。目标元素应存在或使用适合当前版本的延迟目标能力;SSR/水合场景还要保证服务端和客户端结构一致。
Vue 2 没有内置 Teleport,历史项目可使用 portal-vue 等方案,但迁移时要检查库的 Vue 版本和 SSR 支持。
八、响应式原理差异
1. Vue 2:getter/setter
Vue 2 初始化时遍历对象已有属性,用 Object.defineProperty 安装 getter/setter:
const state = {}
let count = 0
Object.defineProperty(state, 'count', {
enumerable: true,
configurable: true,
get() {
return count
},
set(value) {
count = value
},
})
限制包括:
- 初始化后新增对象属性无法被自动转换;
- 删除属性无法被普通 setter 感知;
- 直接设置数组下标和
length有限制; - 需要改写
push、splice等七个数组变异方法; - Vue 2 使用
Vue.set/vm.$set、Vue.delete/vm.$delete处理部分场景。
2. Vue 3:Proxy 与 ref getter/setter
import { reactive, ref } from 'vue'
const state = reactive({ count: 0 })
state.newProperty = '可以直接被追踪'
delete state.newProperty
const count = ref(0)
count.value++
Vue 3 的 reactive 通过 Proxy 拦截对象/数组的 get、set、delete、has 和迭代;Map/Set 由专门的 collection handlers 处理。ref 仍然通过 .value 的访问器属性追踪依赖,所以“Vue 3 完全不使用 getter/setter”也不准确。
reactive() 默认深度转换嵌套对象,shallowReactive/shallowRef 可用于特殊性能或外部状态场景。解构 reactive 对象属性时要注意响应式连接丢失,可使用 toRef/toRefs。
九、虚拟 DOM 与 Vue 3 编译优化
原文中的 Vue 3 渲染函数被抓取成了缺少引号、导入和逗号的无效代码。概念上,下面的模板:
<div id="app">
<div>静态内容</div>
<p>{{ age }}</p>
</div>
可能被编译为类似这样的代码:
const hoisted = createElementVNode('div', null, '静态内容', -1)
function render(_ctx, _cache) {
return openBlock(), createElementBlock('div', { id: 'app' }, [
hoisted,
createElementVNode(
'p',
null,
toDisplayString(_ctx.age),
1, // TEXT patch flag
),
])
}
真实生成代码会根据编译模式、插件、scopeId、SSR 和模板结构不同而变化,建议使用当前 Template Explorer 查看。
Vue 3 的重要运行时提示包括:
- Patch flags:标记动态文本、class、style、props、事件或 Fragment 类型;
- 静态提升:静态 vnode/属性对象只创建一次并复用;
- Block tree:记录动态后代,更新时跳过大量静态节点;
- Keyed diff:对有 key 的子节点使用 key 映射和移动优化;
- 事件处理器缓存:某些编译场景会通过
_cache避免重复创建内联 handler,但不能概括为所有事件都变成静态节点。
Patch flag 判断使用位运算:
if (vnode.patchFlag & PatchFlags.TEXT) {
// 只处理动态文本
}
不是原文中错误的 patchFlag && PatchFlags.KEYED_FRAGMENT。
十、事件和组件通信的变化
Vue 2 组件通信常见 $emit、$on、$off、$once、$listeners、$children 和事件总线。Vue 3 仍支持组件 $emit/emits,但移除了实例事件 API $on、$off、$once,也移除了 $listeners 和 $children。
Vue 3 推荐:
<!-- 子组件 -->
<script setup lang="ts">
const emit = defineEmits<{
change: [value: string]
}>()
emit('change', 'new value')
</script>
跨层通信可以使用 provide/inject、Pinia 或明确的事件/状态服务,不建议把全局事件总线当作所有问题的默认答案。
十一、v-model、过滤器和其他迁移点
1. 组件 v-model
Vue 2 默认使用 value prop + input 事件:
<custom-input v-model="searchText" />
Vue 3 默认使用 modelValue prop + update:modelValue 事件,并支持参数:
<CustomInput v-model="searchText" />
<CustomInput v-model:title="title" />
组件可以使用 defineModel()(Vue 3.4+)简化双向绑定,也可以显式声明 props 和 emits。Vue 2 的 .sync 在 Vue 3 中改为参数化 v-model,但迁移要检查第三方组件 API。
2. 过滤器
Vue 2 的模板过滤器在 Vue 3 中被移除,推荐使用普通函数、computed 或方法:
<template>
<span>{{ formatDate(createdAt) }}</span>
</template>
3. 属性与事件 fallthrough
Vue 3 用 $attrs 同时承载未声明的属性和监听器 fallthrough;组件应通过 inheritAttrs: false 和 v-bind="$attrs" 明确控制多根或特殊组件的属性落点。emits 声明还有助于避免事件被误当作根元素原生监听器。
十二、Tree-shaking 与 TypeScript
1. Tree-shaking
Vue 2 常见的是全局构造函数和不同构建版本:
import Vue from 'vue'
Vue.nextTick(() => {})
Vue 3 使用 ESM 命名导出和应用实例:
import { nextTick } from 'vue'
await nextTick()
现代 bundler 可以在满足条件时移除没有引用的 ESM 代码,但最终体积取决于构建工具、压缩器、编译模式、依赖和应用代码。不要固定宣称 Vue 3 永远只有某个精确 KB 数;也不要把所有内部 API 都当作公共命名导出。
2. TypeScript
Vue 3 代码库使用 TypeScript,Composition API 主要由普通变量、函数和泛型组成,类型推导通常更直接:
<script setup lang="ts">
import { ref } from 'vue'
type User = {
id: number
name: string
}
const user = ref<User | null>(null)
</script>
Vue 2.7 的类型支持已经比早期版本更好,但 Options API、mixin、插件注入和 class API 仍可能让推导复杂。Vue 3 官方不再推荐以 class API 作为主要组件写法,Composition API 和 <script setup> 通常更适合新项目。
十三、Options API 和 Composition API 如何选择
| 场景 | 建议 |
|---|---|
| 小型组件、团队熟悉 Vue 2 写法 | Options API 足够清晰 |
| 复杂组件、多种业务逻辑交叉 | Composition API 更容易按逻辑组织 |
| 需要抽取复用逻辑 | composable |
| 迁移中的旧项目 | 保留 Options API,局部引入 Composition API |
| 新的 Vue 3/Nuxt 4 项目 | 优先考虑 <script setup>,但不是硬性要求 |
Composition API 不是函数式编程,也不是“每次更新都重新调用整个组件函数”的 React Hooks 模型;setup()/<script setup> 通常在组件实例创建时执行一次,响应式 effect 负责后续更新。
总结
- Vue 3 变化很多,但 Options API 仍然受支持;
- Vue 2 的
beforeDestroy/destroyed对应 Vue 3 的beforeUnmount/unmounted; - Vue 3 支持 Fragment、Teleport、Suspense,Suspense 目前仍应视为实验性能力;
- Vue 2 使用
Object.defineProperty,Vue 3 的reactive使用 Proxy,ref仍使用 getter/setter; - Vue 3 的性能优化来自编译器提示、patch flags、静态提升、block tree、调度器和 renderer 协同;
- Vue 3 移除了实例事件总线 API、过滤器、
$listeners和$children; - Vue 3 使用
createApp、命名导出、emits、参数化v-model和更好的 TypeScript 支持; - 迁移不能只替换生命周期名称,应结合组件通信、插件、路由、状态管理、构建和 SSR 一起验证。
参考资料
- Vue 3:Composition API FAQ
- Vue 3:生命周期 API
- Vue 3:迁移指南
- Vue 3:深入响应式原理
- Vue 3:Rendering Mechanism
- Vue 3:全局 API 应用实例
- Vue 2.7 发布说明
- Vue 2 迁移指南
- Vue Template Explorer
原文作者:cv搬砖工。原文生命周期、Fragment、Composition API、Suspense、Teleport、响应式、VDOM、事件缓存、Diff、Tree-shaking 和 TypeScript 的章节予以保留;损坏的模板/渲染函数、Vue 3 性能倍数的绝对结论、事件缓存的过度概括、patchFlag 位运算错误以及 Vue 3 会淘汰 Options API 等说法已修正。