vue2 + vue3差异点
Vue 2 到 Vue 3 是一次底层架构全面重写,同时带来了大量新特性和思维方式的转变。下面从核心机制、API 设计、编译优化、全局特性等维度,逐一深入对比,并结合源码逻辑或伪代码辅助理解。
1. 响应式系统:从Object.defineProperty到Proxy
Vue 2 响应式原理
- 使用
Object.defineProperty劫持对象属性的 getter/setter。 - 初始化时递归遍历所有属性,为每个属性单独设置 getter/setter,依赖收集和派发更新。
- 缺点:
- 无法检测对象属性的添加/删除(需用
Vue.set/delete)。 - 对数组的索引赋值和
length修改无法监听,通过重写数组的 7 种变更方法弥补。 - 深层递归遍历性能开销大,初始化慢。
- 无法检测对象属性的添加/删除(需用
简化源码思路:
functiondefineReactive(obj,key,val){constdep=newDep()Object.defineProperty(obj,key,{get(){dep.depend()returnval},set(newVal){val=newVal dep.notify()}})}Vue 3 响应式原理
- 使用 ES6
Proxy代理整个对象,可拦截 13 种操作。 - 依赖收集通过
track,触发通过trigger,使用WeakMap→Map→Set结构存储依赖。 - 优点:
- 可监听属性的增加、删除。
- 直接监听数组索引和
length。 - 惰性响应式:只有访问到的深层属性才会被递归代理(
reactive仅在 get 时递归)。 - 支持
Map、Set、WeakMap、WeakSet等复杂类型。
核心代码模拟:
functionreactive(target){returnnewProxy(target,{get(target,key,receiver){constres=Reflect.get(target,key,receiver)track(target,key)// 依赖收集returnisObject(res)?reactive(res):res// 懒递归},set(target,key,val,receiver){constoldVal=target[key]constresult=Reflect.set(target,key,val,receiver)if(val!==oldVal)trigger(target,key)returnresult},deleteProperty(target,key){consthad=Object.prototype.hasOwnProperty.call(target,key)constresult=Reflect.deleteProperty(target,key)if(had)trigger(target,key,'delete')returnresult}})}ref则是对基本类型值包裹一个对象,通过getter/setter访问.value,内部仍然走reactive的逻辑处理对象值。
2. Composition API vs Options API
Vue 2 Options API
- 组件逻辑按选项(
data,methods,computed,watch, 生命周期)分割。 - 代码复用主要靠 mixin,存在命名冲突、来源不清晰等问题。
Vue 3 Composition API
- 按逻辑关系组织代码,一个功能的响应式状态、方法、监听集中在一起。
- 逻辑复用通过自定义组合函数(hooks),没有 mixin 的冲突。
setup函数为入口,在组件实例创建前执行,this不可用。- 提供
ref,reactive,computed,watch,watchEffect, 生命周期钩子(onMounted等)。
示例对比:
// Vue 2 Options APIexportdefault{data(){return{count:0}},computed:{double(){returnthis.count*2}},methods:{increment(){this.count++}}}// Vue 3 Composition APIimport{ref,computed}from'vue'exportdefault{setup(){constcount=ref(0)constdouble=computed(()=>count.value*2)constincrement=()=>count.value++return{count,double,increment}}}Vue 3 也完全兼容 Options API,两者可在同一组件中使用,但 Composition API 能更好地组织和复用逻辑,并且天然支持 Tree-shaking。
3. 生命周期钩子变化
| Vue 2 | Vue 3 Composition 钩子 | 说明 |
|---|---|---|
| beforeCreate | 不需要,相当于setup()开始 | 此时组件实例刚创建,data未初始化,Vue 3 中setup执行时机替代它 |
| created | 不需要 | setup末尾可替代 |
| beforeMount | onBeforeMount | 模板编译/渲染前 |
| mounted | onMounted | DOM 挂载后 |
| beforeUpdate | onBeforeUpdate | 数据更新前 |
| updated | onUpdated | 数据更新导致 DOM 更新后 |
| beforeDestroy | onBeforeUnmount | 组件销毁前(Vue 3 重命名为 unmount) |
| destroyed | onUnmounted | 组件销毁后 |
| errorCaptured | onErrorCaptured | 捕获子组件错误 |
setup中可同步使用这些钩子函数,也新增了onRenderTracked和onRenderTriggered用于调试响应式依赖。
4. Fragments:多根节点支持
Vue 2:组件模板必须有且仅有一个根元素,否则报错。
<!-- Vue 2 会报错 --><template><div>A</div><div>B</div></template>Vue 3:组件可返回多个根节点,内部自动创建 Fragment 虚拟节点包裹。
<!-- Vue 3 合法 --><template><div>A</div><div>B</div></template>编译结果会生成一个Fragment类型的 vnode,patch时只处理子节点数组,不生成额外的 DOM 元素。这减少无意义的包裹 DOM,使组件更灵活。
5. Teleport(传送门)
Vue 3 新增内置组件<Teleport>,可将子节点渲染到 DOM 中特定目标位置,常用于模态框、通知等需要脱离父组件层级的场景。
<template><button@click="show = true">打开弹窗</button><Teleportto="body"><divv-if="show"class="modal">我是被传送到 body 下的内容</div></Teleport></template>原理:Teleport的process过程中,会根据to选择器找到目标容器,使用move方法将子节点移动过去,且保持响应式绑定。
6. Suspense(异步组件协调)
Vue 3 引入<Suspense>,用于协调异步组件,展示加载状态。
<Suspense><template#default><AsyncComponent/></template><template#fallback><div>Loading...</div></template></Suspense>Suspense可以捕获setup中返回的Promise,在未 resolve 前显示 fallback,并支持嵌套、错误处理。这在 SSR 中实现流式传输也有重要作用。
7. 自定义渲染器(Custom Renderer)
Vue 2 的渲染器与平台(Web)强绑定,虽然通过 weex 有一些分离,但不够彻底。
Vue 3 将运行时拆分为@vue/runtime-core(平台无关)和@vue/runtime-dom(Web),通过自定义nodeOps和patchProp可以创建自定义渲染器,比如渲染到 Canvas、Native。
核心接口:
import{createRenderer}from'@vue/runtime-core'const{createApp}=createRenderer({createElement(type){...},insert(child,parent,anchor){...},patchProp(el,key,prev,next){...}})这让 Vue 3 不局限于 DOM,可轻松移植到其他平台。
8. 虚拟 DOM 与编译优化
Vue 3 的编译器进行了大幅性能优化,生成的代码配合运行时,使得更新粒度更细。
- 静态提升:模板中不变的节点提升到渲染函数外,避免每次重新创建 vnode。
- 预字符串化:大量连续静态节点会被直接编译成 HTML 字符串,通过
innerHTML一次性创建。 - Patch Flags(补丁标记):动态节点打上标志位(如
1代表文本动态,2代表 class 动态,32代表属性动态),更新时只比对所需部分,跳过静态内容。 - 事件缓存:将事件处理函数缓存,避免重新绑定。
- Block Tree:把动态节点收集到一个扁平数组中,更新时直接遍历该数组,跳过静态节点比对,diff 性能接近 O(n)(n 为动态节点数)。
编译示例:
<div><span>静态</span><span>{{ msg }}</span></div>编译后的渲染函数近似:
import{createElementVNodeas_c,toDisplayStringas_t}from'vue'const_hoisted_1=_c('span',null,'静态')exportfunctionrender(_ctx){return(_openBlock(),_c('div',null,[_hoisted_1,_c('span',null,_t(_ctx.msg),1/* TEXT */)]))}带有1 /* TEXT */的标志位,运行时只检查textContent。
9. TypeScript 支持
Vue 2 使用 Flow 做类型检查,对 TS 支持依赖官方声明文件和vue-class-component,体验一般,类型推导差。
Vue 3 使用 TypeScript 重写整个源码,提供完善的类型定义和推导。
defineComponent提供完备的 Props、组件实例类型推导。- Composition API 中
ref<T>()、reactive<T>()等可正确推导类型。 defineProps,defineEmits在<script setup>中纯类型声明,无需运行时定义。
示例:
constprops=defineProps<{title:string}>()10. 全局 API 变化与 Tree-shaking
Vue 2全局 API 都挂载在Vue对象上:
importVuefrom'vue'Vue.component('MyComp',{...})Vue.directive('focus',{...})Vue.mixin({...})newVue({render:h=>h(App)}).$mount('#app')这种方式将全部 API 打包在一起,无法 tree-shake,且多个应用共享同一个 Vue 全局配置,易相互污染。
Vue 3采用具名导出 +createApp工厂函数:
import{createApp,defineComponent,directive}from'vue'constapp=createApp(App)app.component('MyComp',{...})app.directive('focus',{...})app.mount('#app')createApp返回独立的应用实例,配置隔离,互不影响。未使用的 API 如transition,provide等可以通过构建工具 Tree-shaking 移除。
11. v-model 变化
Vue 2
- 一个组件只能有一个
v-model,默认绑定valueprop 和input事件。 - 自定义
model选项:model:{prop:'visible',event:'change'} .sync修饰符实现“双向绑定”其他 prop。
Vue 3
- 移除
model选项,统一为v-model的变体。 - 可同时使用多个
v-model:v-model:title,v-model:visible。 - 默认绑定
modelValueprop 和update:modelValue事件。 - 移除了
.sync修饰符,由v-model:propName代替。
<!-- Vue 3 --><ChildComponentv-model:title="title"v-model="visible"/>子组件:
props:['modelValue','title'],emits:['update:modelValue','update:title']12. 事件总线(EventBus)移除
Vue 2 中常用new Vue()作为事件总线:
constbus=newVue()bus.$on('event',handler)bus.$emit('event',data)Vue 3 移除了$on,$off,$once,官方推荐使用mitt等第三方库或provide/inject、全局状态管理代替,原因:实例的监听会造成内存泄漏风险,且不符合 Composition API 设计。
13. 函数式组件变化
Vue 2:函数式组件无状态、无实例,通过functional: true定义,render接收context。
exportdefault{functional:true,render(h,ctx){returnh('div',ctx.props.msg)}}Vue 3:函数式组件简化为普通函数,不再需要functional标志,性能与有状态组件差距极小(Vue 3 的组件初始化非常轻量)。
import{h}from'vue'constFuncComp=(props)=>h('div',props.msg)14. 组件注册与异步组件
Vue 2局部注册仅支持选项对象,异步组件:
Vue.component('async-comp',()=>import('./Comp.vue'))Vue 3通过defineComponent辅助类型推导,异步组件使用defineAsyncComponent:
import{defineAsyncComponent}from'vue'constAsyncComp=defineAsyncComponent(()=>import('./Comp.vue'))defineAsyncComponent支持高级选项:loadingComponent,errorComponent,delay,timeout等。
15. $attrs 与 $listeners
Vue 2:$attrs包含未被 props 接收的 attribute,$listeners包含父组件传递的事件监听器(不含.native修饰符的)。
Vue 3:
- 移除
$listeners,事件监听器现在也是$attrs的一部分,以onXxx的形式存在。 - 移除
.native修饰符,如果未在emits声明的事件,会被视为原生事件,可直接在$attrs的onClick获取。
这统一了属性和事件透传,方便更高阶的组件封装。
16. 其他细节变化
- key 属性:Vue 3 在
<template v-for>上现在推荐(也可以)将key放在<template>标签上,不再必须放在子元素。 - v-if vs v-for 优先级:Vue 2 中
v-for优先级高于v-if;Vue 3 中v-if优先级更高,当两者同处一个元素时,v-if先评估,可能导致错误。 - 渲染函数 API:Vue 2 使用
render(h) { return h('div', ...) },Vue 3 直接从vue引入h。 - Transition 类名变化:
v-enter改为v-enter-from,v-leave改为v-leave-from,语义更清晰。
17. 源码架构与构建
Vue 3 采用 monorepo 管理,使用 TypeScript 重写,按功能拆分包:
@vue/reactivity— 响应式系统@vue/runtime-core— 平台无关运行时@vue/runtime-dom— Web 运行时@vue/compiler-core— 平台无关编译器@vue/compiler-dom— Web 编译器@vue/runtime-test— 测试用渲染器
这种分层使得各模块可独立使用、测试和 Tree-shaking,也方便自定义渲染器。
总结
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | defineProperty,无法检测增删,数组需要 hack | Proxy,全面劫持,惰性收集,支持 Map/Set |
| API 风格 | Options API | 新增 Composition API,兼容 Options API |
| 根节点 | 单根 | 多根 Fragment |
| 类型支持 | Flow,TS 体验一般 | TypeScript 重写,完美类型推导 |
| 编译优化 | 无 | 静态提升、Patch Flag、Block Tree 等 |
| 全局 API | Vue构造函数,多应用共享配置 | createApp实例隔离,按需导出 |
| 传送/异步协调 | 无内置 | Teleport、Suspense |
| 自定义渲染 | 耦合度高 | 通过createRenderer自定义 |
| v-model | 单个.sync补充 | 多个v-model:attr,移除.sync |
| 事件总线 | $on/$off/$emit | 移除,推荐 mitt 或 provide |
| 包大小 | 全量约 23KB gzip | 借助 tree-shaking 可更低 (约 10KB+) |
Vue 3 从底层到上层全面进化,不仅带来了更好的性能、更优的 TypeScript 集成,也通过 Composition API 和丰富的内置组件极大提升了开发体验与代码组织能力。
