Vue3 核心变化一览
Vue3 版本重构核心基于两大核心考量,解决 Vue2 长期存在的架构短板、适配前端技术发展:
- 技术迭代适配:依托 ES6+ 全新语言特性,弥补 ES5 语法的能力局限;
- 修复架构缺陷:解决 Vue2 响应式局限、代码复用弱、大型项目可维护性差、TS 支持不完善等历史问题。
Vue2 已于 2023 年 12 月 31 日停止官方维护,Vue3 是目前唯一官方主推、持续迭代的稳定版本。
核心优势与新特性
- 性能更强:重写虚拟 DOM、优化模板编译、静态节点标记、事件缓存,更新性能提升 1.3~2 倍,SSR 渲染速度提升 2~3 倍;
- 体积更小:全面支持 Tree-Shaking,按需打包,剔除未使用 API,大幅减小生产包体积;
- 维护性更高:组合式 API 实现逻辑集中管理,完美适配大型项目,TS 原生支持更友好;
- 能力更全面:暴露底层响应式 API,贴近原生 DOM,支持自定义渲染器,拓展性极强。
- 组合式 API(Composition API):替代分散的选项式 API,实现逻辑聚合、高效复用;
<script setup>语法糖:组合式 API 简化写法,无需 return、自动解析顶层变量;- Fragments 多根节点:模板支持多个根标签,无需多余外层包裹节点;
- Teleport 瞬移组件:将组件 DOM 挂载到 app 外任意节点(常用于弹窗、遮罩层);
- Emits 事件声明:显式声明组件自定义事件,规范事件传递,替代 Vue2 隐式事件;
- SFC 状态驱动 CSS:style 中可通过
v-bind绑定组件变量,实现动态样式; - 增强 scoped 样式:支持 scoped 样式内写全局样式、插槽专属样式;
- Suspense(实验性):优雅处理异步组件加载状态,配置 loading 兜底展示;
- 自定义渲染器 createRenderer:脱离浏览器 DOM,适配小程序、桌面端等多端渲染。
Vue3 框架级官方推荐变更
Vue3 生态全面迭代,官方更新全套工程化工具链方案:
- 构建工具:Vue CLI(维护态)→Vite(极速构建、热更新)。
- 状态管理:Vuex → Pinia(轻量化、无嵌套、TS 友好)。
- IDE 插件:Vetur → Volar(完善 TS 类型推导、语法校验)。
- 静态站点:VuePress → VitePress。
- JSX 支持:@vue/babel-preset-jsx → @vue/babel-plugin-jsx。
- TS 校验:新增 vue-tsc 命令行类型校验工具。
Vue2 与 Vue3 核心差异
响应式原理(最核心差异)
Vue2:Object.defineProperty
遍历对象已有属性,劫持 getter/setter 实现响应式,存在先天缺陷:
- 无法监听对象新增/删除属性,需依赖
$set/$delete; - 无法监听数组下标修改、数组 length 变更,仅重写 7 个数组变异方法;
- 需递归遍历嵌套对象,深层监听性能开销大;
- 兼容 IE 浏览器,无法适配新语法特性。
Vue3:Proxy
劫持整个对象,返回全新代理对象,完美解决 Vue2 所有响应式问题:
- 支持监听对象新增、删除属性,无需额外 API;
- 原生监听数组下标、length 变更,支持所有数组操作;
- 支持 Map、Set、WeakMap、WeakSet 数据结构响应式;
- 惰性递归监听,优化深层对象性能;
- 缺点:不兼容 IE 浏览器,无完整 polyfill 方案。
API 设计
选项式 API(Vue2 主打)
按功能选项拆分代码(data/methods/computed/watch),同一业务逻辑分散在不同模块,大型项目代码散乱、难以复用和维护,依赖 this 上下文,TS 类型推导薄弱。
组合式 API(Vue3 主推)
按业务逻辑聚合代码,可抽离独立组合函数复用逻辑,无 this 依赖,原生适配 TS,压缩打包更友好。
逻辑复用示例(鼠标位置监听):
// 复用逻辑 useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
const x = ref(0)
const y = ref(0)
const update = (e) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
<!-- 组件使用 -->
<template>
<div>鼠标坐标:{{ x }} {{ y }}</div>
</template>
<script setup>
import useMousePosition from './useMousePosition'
const { x, y } = useMousePosition()
</script>
选型建议:Vue3 兼容两种 API,小型项目可用选项式 API,中大型项目优先组合式 API,官方不废弃选项式 API。
模板与 DOM 变更
- 多根节点支持:Vue2 模板必须单根节点,Vue3 支持 Fragments 多根节点;
- v-if / v-for 优先级:Vue2 v-for 优先级更高,Vue3 v-if 优先级更高(规避无效循环,优化性能);
- key 规则优化:v-if/v-else 分支无需手动 key,Vue3 自动生成唯一 key;
<template v-for>的 key 需写在 template 标签上; - v-bind 顺序敏感:Vue3 后声明的属性会覆盖先声明的同名属性,Vue2 固定优先级;
- 移除 .native 修饰符:未在 emits 声明的事件,自动绑定到组件根元素。
生命周期变更
Vue3 组合式 API 生命周期需手动导入,命名统一增加 on 前缀,移除 beforeCreate、created(逻辑直接写在 setup 中):
| Vue2 生命周期 | Vue3 组合式生命周期 |
|---|---|
| beforeCreate / created | setup(替代,无需手动声明) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeDestroy | onBeforeUnmount |
| destroyed | onUnmounted |
v-model(重点)
Vue3 重构组件双向绑定,移除 .sync 和组件 model 选项,统一 v-model 规范:
- 默认参数变更:Vue2
:value + @input→ Vue3:modelValue + @update:modelValue; - 支持多 v-model:单个组件可绑定多个双向绑定;
- 支持自定义修饰符:可自定义 capitalize、trim 等专属修饰符;
- 语法糖简化:Vue3.4+ 新增
defineModel宏,极简实现双向绑定。
<!-- Vue3 组件 v-model -->
<Child v-model="title" />
<!-- 编译等价于 -->
<Child :modelValue="title" @update:modelValue="val => title = val" />
<!-- 带参数、多绑定 -->
<Child v-model:title="name" v-model:content="desc" />
setup 核心语法
setup 是组合式 API 的核心入口,执行时机早于 beforeCreate,无组件实例 this。
核心特性:
- 无
this,无法访问 data、methods、computed; - 接收两个参数:props(响应式)、context(非响应式);
- props 不可直接解构(丢失响应式),需配合
toRefs解构; - context 包含 attrs、slots、emit,可直接解构使用;
- 返回变量/方法可直接在模板使用,支持返回渲染函数。
import { ref, reactive, toRefs } from 'vue'
export default {
props: ['title'],
setup(props, { attrs, slots, emit }) {
// 保留props响应式解构
const { title } = toRefs(props)
// 定义响应式数据
const count = ref(0)
const state = reactive({ name: 'Vue3' })
// 定义方法
const handleClick = () => count.value++
// 暴露模板使用
return { count, state, handleClick }
}
}
重大破坏性变更
全局 API 重构(Tree-Shaking 友好)
移除 Vue2 全局静态 API,改为实例 API + 按需导入,解决全局污染、无法按需打包问题:
// Vue2
import Vue from 'vue'
Vue.component('xxx', xxx)
Vue.nextTick(() => {})
// Vue3
import { createApp, nextTick } from 'vue'
const app = createApp({})
app.component('xxx', xxx)
组件相关变更
- 异步组件:必须通过
defineAsyncComponent创建,废弃 Vue2 简易写法; - 函数式组件:移除
functional配置,仅支持纯函数写法; - 插槽统一:移除
$scopedSlots,所有插槽统一通过$slots访问(函数形式); - $listeners 移除:事件监听器合并入
$attrs,$attrs 新增包含 class、style。
样式与动画变更
- transition 类名变更:
v-enter→v-enter-from、v-leave→v-leave-from; - TransitionGroup:默认不渲染根 DOM 节点,无需冗余外层标签。
自定义指令变更
指令钩子对齐组件生命周期,重命名所有钩子,新增两个生命周期钩子:
- bind → beforeMount;
- inserted → mounted;
- componentUpdated → updated;
- unbind → unmounted;
- 移除 update 钩子,新增 beforeUpdate、beforeUnmount 钩子;
- 删除
binding.expression属性。
彻底移除的 API
- 事件相关:
$on、$off、$once(废弃全局事件总线); - 响应式相关:
$set、$delete(Proxy 无需手动处理); - 工具相关:
filters过滤器、keyCode 按键修饰符、inline-template; - 实例相关:
$destroy、$children、propsData。
常见问题
Vue3 如何实现 Vue2 created 生命周期?
Vue3 移除 beforeCreate、created 钩子,setup 函数执行时机等价于这两个钩子,初始化数据、请求、监听等逻辑直接写在 setup 顶层即可。
Vue3 监听数组为何不触发更新?
Vue3 的 watch 默认仅监听数组整体替换,数组内部元素修改、长度变更不会触发回调,如需监听内部变化,必须开启 deep: true。
Vue3 相比 Vue2 性能提升的核心原因?
- Proxy 惰性响应式,减少递归监听开销;
- 虚拟 DOM 优化:静态节点标记、PatchFlag 精准 diff、事件缓存;
- Tree-Shaking 按需打包,减小体积、提升运行速度;
- v-if 优先级高于 v-for,规避无效循环渲染。
标准模板
<template>
<a-modal
v-model:visible="reactData.visible"
title="标题"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form
ref="refForm"
:model="reactData.formData"
:rules="formRules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
autocomplete="off"
>
...
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, defineEmits, defineProps, reactive, watch, computed } from 'vue';
import type { FormInstance } from 'ant-design-vue';
const formRules = {}
const refForm = ref<FormInstance>()
// props
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false
}
})
// emits
const emits = defineEmits(['update:visible'])
// data
const reactData = reactive({
loading: false,
visible: false,
formData: {
keyword: '',
birthMonth: ''
}
})
// watch
watch(() => props.visible, (newValue, oldValue) => {
reactData.visible = newValue
})
// computed
let isPasswordSame = computed(() => {
return reactData.formData.password === reactData.formData.newPassword
})
// methods
function handleCancel () {}
function handleOk () {}
</script>