Vue 3 深入
概述
Vue 3 是 Evan You 领导开发的渐进式 JavaScript 框架的第三个主要版本,于 2020 年 9 月正式发布。与 Vue 2 相比,Vue 3 在核心响应式系统、运行时性能和开发体验上实现了质的飞跃。本文将从响应式原理、组合式 API、依赖追踪机制、编译优化等维度深入剖析 Vue 3 的设计与实现。
响应式原理
Proxy vs Object.defineProperty
Vue 2 使用 Object.defineProperty 实现响应式系统,而 Vue 3 改用 ECMAScript 2015 引入的 Proxy 对象。这一改动是 Vue 3 最底层、最重要的架构变化。
| 对比维度 | Object.defineProperty (Vue 2) | Proxy (Vue 3) |
|---|---|---|
| 拦截粒度 | 属性级别,需逐个定义 getter/setter | 对象级别,代理整个对象的全部操作 |
| 新增属性 | 无法自动拦截,需调用 Vue.set | 自动拦截 set 操作,无需额外 API |
| 删除属性 | 无法拦截 delete 操作 | 支持 deleteProperty 陷阱 |
| 数组支持 | 需重写数组方法(push/pop 等),无法拦截索引赋值 | 原生支持数组索引和 length 变更 |
| 初始化开销 | 递归遍历所有属性(深度越大开销越大) | 惰性代理,访问到深层时才递归 |
| 性能上限 | 大数据量场景有显著性能瓶颈 | 通常更快,尤其对大对象和频繁操作 |
Vue 2 实现原理简化:
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
// 依赖收集
track(obj, key)
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
// 派发更新
trigger(obj, key)
}
}
})
}Vue 3 实现原理简化:
function reactive(target) {
const handler = {
get(target, key, receiver) {
// 依赖收集
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 派发更新
trigger(target, key)
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
// 派发更新
trigger(target, key)
return result
}
}
return new Proxy(target, handler)
}reactive
reactive 是 Vue 3 创建响应式对象的核心 API,它接收一个普通对象并返回一个 Proxy 包装后的响应式代理。reactive 的转换是深层的——当访问到嵌套对象时,内层对象也会被递归地转换为响应式。
实现要点:
- 缓存机制:已代理过的对象会被缓存到
reactiveMap(WeakMap)中,避免重复代理。 - 同一代理引用:多次对同一对象调用
reactive,返回同一 Proxy 实例。 - 已有代理检测:如果目标已经是 Proxy,且是 Vue 代理,直接返回。
- 仅代理对象:只能代理
Object、Array、Map、Set、WeakMap、WeakSet等引用类型,原始值(string、number、boolean 等)无法直接代理。
// 源码大致流程
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
// 非对象直接返回
if (!isObject(target)) return target
// 已经是响应式对象,直接返回
if (target[ReactiveFlags.IS_REACTIVE]) return target
// 从缓存中获取
const existingProxy = reactiveMap.get(target)
if (existingProxy) return existingProxy
// 创建 Proxy
const proxy = new Proxy(target, handlers)
// 缓存并返回
reactiveMap.set(target, proxy)
return proxy
}ref
ref 用于处理原始值的响应式。由于 Proxy 无法直接代理原始值,Vue 3 将原始值包装在一个具有 value 属性的对象中,然后对该对象进行 reactive 代理。
function ref(value) {
return createRef(value, false)
}
function createRef(rawValue, shallow) {
if (isRef(rawValue)) return rawValue
return new RefImpl(rawValue, shallow)
}
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isRef = true
this._rawValue = value
this._value = toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = toReactive(newVal)
triggerRefValue(this)
}
}
}当 ref 的值是对象类型时,内部会调用 toReactive——即 reactive 方法将其深层转换为响应式对象。这意味着 ref 不仅可以包装原始值,也可以持有复杂对象。
toRef / toRefs
toRef 和 toRefs 用于保持响应式对象的属性与原始属性的响应式连接,在解构或传递属性时保持响应性。
toRef:将响应式对象上的某个属性转换为独立的 ref,并维持响应式连接。
function toRef(object, key, defaultValue) {
const val = object[key]
return isRef(val) ? val : new ObjectRefImpl(object, key, defaultValue)
}
class ObjectRefImpl {
constructor(_object, _key, _defaultValue) {
this._object = _object
this._key = _key
this._defaultValue = _defaultValue
this.__v_isRef = true
}
get value() {
const val = this._object[this._key]
return val === undefined ? this._defaultValue : val
}
set value(newVal) {
this._object[this._key] = newVal
}
}toRefs:将响应式对象的所有属性转换为 ref 对象。常用于从 reactive / setup 返回值解构时保持响应性。
function toRefs(object) {
const ret = Array.isArray(object)
? new Array(object.length)
: {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}典型使用场景:
const state = reactive({ count: 0, name: 'Vue' })
// 解构后仍然保持响应性
const { count, name } = toRefs(state)
count.value++ // 同时更新 state.countComposition API
Composition API 是 Vue 3 引入的一组全新的逻辑复用与组织方式,与 Vue 2 的 Options API 并存。
setup
setup 是 Composition API 的入口函数,在组件实例创建之前执行。它接收两个参数:props(响应式)和 context(包含 attrs、slots、emit、expose)。
export default {
setup(props, context) {
// props 是响应式的
// context 是普通对象,包含 attrs, slots, emit, expose
return {
// 返回的对象暴露给模板和 Options API
}
}
}在 <script setup> 语法中,setup 变为顶层上下文,导入的变量和函数可以直接在模板中使用:
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
</script>
<template>
<p>{<!-- v-pre -->{ count }} x 2 = {<!-- v-pre -->{ double }}</p>
</template>ref
ref 用于创建响应式引用。在模板中访问时自动解包(无需 .value),但在 JavaScript 中需要通过 .value 访问。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1reactive
reactive 用于创建响应式对象。与 ref 不同,reactive 直接操作属性(无需 .value),但只接受对象类型。
const state = reactive({
count: 0,
name: 'Vue 3'
})
console.log(state.count) // 0
state.count++computed
computed 创建一个基于其他响应式数据的派生状态。它接收一个 getter 函数或包含 get/set 的对象。
// 只读计算属性
const double = computed(() => count.value * 2)
// 可写计算属性
const plusOne = computed({
get: () => count.value + 1,
set: (val) => { count.value = val - 1 }
})computed 返回的是一个 ComputedRefImpl 实例,本质是一个特殊的 ref。它惰性求值——只有当依赖变化且计算属性被访问时才会重新计算。
watch / watchEffect
watch:监听特定的响应式数据源,当数据变化时执行回调函数。可以获取新旧值。
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 监听 reactive 对象属性(需要传入 getter)
watch(
() => state.count,
(newVal, oldVal) => console.log(newVal, oldVal)
)
// 监听多个源
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(newCount, oldCount, newName, oldName)
})watchEffect:立即执行传入的函数,并自动追踪其内部使用的所有响应式依赖。不需要指定监听的目标。
const stop = watchEffect(() => {
// 自动追踪 count 和 state.name
console.log(`count = ${count.value}, name = ${state.name}`)
})
// 停止监听
stop()watch vs watchEffect 的核心区别:
| 特性 | watch | watchEffect |
|---|---|---|
| 懒执行 | 默认懒执行(除非传 immediate) | 立即执行 |
| 依赖声明 | 显式指定监听源 | 隐式自动收集 |
| 新旧值 | 可以获取新旧值 | 只获取当前值 |
| 清理时机 | 需手动处理 | 组件卸载自动停止 |
生命周期 Hooks
Vue 3 的 Composition API 提供了与 Options API 对应的生命周期钩子函数,均需在 setup 中同步调用。
| Options API | Composition API |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
| activated | onActivated |
| deactivated | onDeactivated |
使用示例:
import { onMounted, onUnmounted, onBeforeUpdate } from 'vue'
export default {
setup() {
onMounted(() => console.log('组件已挂载'))
onBeforeUpdate(() => console.log('组件即将更新'))
onUnmounted(() => console.log('组件已卸载'))
}
}响应式依赖追踪机制
Vue 3 的响应式系统核心包括三个概念:effect、track 和 trigger。
effect
effect 是 Vue 3 响应式系统的最小执行单元。它包装一个函数,当函数中使用的响应式数据发生变化时,该函数会被重新执行。
// 简化实现
class ReactiveEffect {
constructor(fn) {
this.fn = fn
this.deps = []
this.active = true
}
run() {
if (!this.active) return this.fn()
try {
activeEffect = this
return this.fn()
} finally {
activeEffect = undefined
}
}
}组件的渲染函数、computed、watch、watchEffect 本质上都是 effect 的包装。
依赖收集(track)
当在 effect 执行过程中访问响应式对象的属性时,track 函数被调用,建立 target → key → effect 的三层映射关系。
// 全局数据结构
const targetMap = new WeakMap() // target -> key -> effects
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
if (!deps.has(activeEffect)) {
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
}数据结构层次:
targetMap (WeakMap)
└─ target (对象)
└─ depsMap (Map)
└─ key (属性名)
└─ deps (Set<ReactiveEffect>)
└─ effect1, effect2, ...派发更新(trigger)
当响应式对象的属性值发生变化时,trigger 函数被调用,找出与该属性相关的所有 effect 并重新执行。
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
if (effects) {
const effectsToRun = new Set(effects)
effectsToRun.forEach(effect => {
if (effect !== activeEffect) {
effect.run()
}
})
}
}依赖清理
每次 effect 重新执行前,会清理自身的旧依赖,然后重新收集当前依赖。这是保证依赖准确性的关键——避免因条件分支导致的无效更新。
function cleanupEffect(effect) {
const { deps } = effect
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}执行流程总结
- 初始化阶段:组件渲染时,渲染
effect开始执行(调用 render 函数)。 - 追踪阶段:模板访问响应式数据(如
reactive对象属性),Proxy 的get陷阱触发track,收集当前活跃的effect。 - 变更阶段:用户修改数据,Proxy 的
set陷阱触发trigger。 - 更新阶段:
trigger查找与该 key 关联的所有effect,依次重新执行,触发组件重新渲染或计算属性重新计算。 - 清理阶段:每次
effect重新执行前,清理旧依赖,执行后重新收集。
编译优化
Vue 3 在模板编译阶段进行了大量优化,使得运行时更新粒度从 Vue 2 的"组件级"降为"节点级"甚至"动态节点级"。
PatchFlag
编译时,Vue 3 为每个动态绑定标记一个 PatchFlag(更新类型标记),运行时根据标记直接执行对应类型的更新操作,避免无意义的 diff 比较。
<div :id="dynamicId" :class="dynamicClass" :style="dynamicStyle">
{<!-- v-pre -->{ msg }}
</div>编译后:
createElementVNode('div', {
id: _ctx.dynamicId,
class: _ctx.dynamicClass,
style: _ctx.dynamicStyle
}, _toDisplayString(_ctx.msg), 7 /* TEXT | CLASS | STYLE | PROPS */)其中 7 是 PatchFlag 的位运算组合值,表示该节点只需要检查文本、class、style 和 props 的变化,不需要检查其他内容。
// PatchFlag 枚举(部分)
const PatchFlags = {
TEXT: 1, // 动态文本
CLASS: 2, // 动态 class
STYLE: 4, // 动态 style
PROPS: 8, // 动态 props
FULL_PROPS: 16, // 动态 key
HYDRATE_EVENTS: 32,
STABLE_FRAGMENT: 64,
KEYED_FRAGMENT: 128,
UNKEYED_FRAGMENT: 256,
NEED_PATCH: 512,
DYNAMIC_SLOTS: 1024,
// ...
}Block Tree
Vue 3 将模板结构划分为 Block(块级节点),每个 Block 收集其内部的动态子节点(通过 dynamicChildren 数组),更新时只遍历 dynamicChildren 而非整棵子树。
<div> <!-- Block Root -->
<span>static</span> <!-- 静态节点,无 PatchFlag -->
<span :id="id">dynamic</span> <!-- 动态节点,加入 dynamicChildren -->
<div v-if="ok"> <!-- v-if 创建新的 Block -->
<p>{<!-- v-pre -->{ name }}</p>
</div>
</div>典型 Block 节点包括:根节点、v-if、v-for、v-else-if、v-else、<template v-for> 等。
优化效果:树型 diff 的比对范围从整棵树缩小到 Block 的 dynamicChildren 列表,大大减少比较次数。
静态提升
Vue 3 编译时会自动将纯静态的节点或子树提取到渲染函数之外,避免每次渲染时重复创建 VNode。
<p class="static-text" data-info="hello">纯静态文本</p>编译为:
// 静态提升到外部
const _hoisted_1 = createElementVNode('p',
{ class: 'static-text', 'data-info': 'hello' },
'纯静态文本', -1 /* HOISTED */
)
function render(_ctx, _cache) {
return _hoisted_1 // 直接复用
}静态节点标记 -1(HOISTED)意味着它永远不会参与更新。
缓存事件处理
Vue 3 会自动缓存内联事件处理函数,避免因每次渲染创建新函数导致的子组件不必要重渲染。
<MyButton @click="count++" />编译为:
createVNode(MyButton, {
onClick: _cache[0] || (_cache[0] = () => (_ctx.count++))
})事件处理函数首次渲染时创建并缓存到 _cache 数组中,后续渲染直接复用缓存的函数引用。
v-once 和 v-memo
- v-once:标记元素/组件只渲染一次,后续更新跳过。
- v-memo(3.2+):根据依赖数组判断是否跳过当前块的更新,用于更精细的缓存控制。
Vue 2 与 Vue 3 核心差异对比
| 对比维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式实现 | Object.defineProperty + 数组方法重写 | Proxy |
| 新增属性检测 | 不支持,需 Vue.set | 原生支持 |
| 数组索引赋值 | 不支持 | 原生支持 |
| 删除属性检测 | 不支持,需 Vue.delete | 原生支持 |
| TypeScript 支持 | 有限(通过 vue-class-component) | 一等支持(源码用 TS 重写) |
| API 风格 | Options API | Options API + Composition API |
| 逻辑复用 | mixins(命名冲突,来源不清晰) | Composable 函数 |
| Fragment | 不支持(模板需单根节点) | 支持多根节点 |
| Teleport | 需第三方库 | 内置 <Teleport> |
| Suspense | 不支持 | 内置 <Suspense> |
| 多 v-model | 不支持 | 支持(v-model:xxx) |
| Emits 声明 | 无 | 支持 emits 选项 |
| 指令钩子 | bind/inserted/update/componentUpdated/unbind | created/beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted |
| 全局 API | Vue.prototype / Vue.mixin / Vue.use | createApp 返回的 app 实例 |
| Tree-shaking | 未使用 API 无法被摇掉 | 支持 Tree-shaking,按需引入 |
| Diff 算法 | 全量比较虚拟 DOM | PatchFlag + Block Tree + 静态提升 |
| 包体积 | ~30 KB (gzip) | ~13 KB (gzip, 仅运行时 + Composition API) |
| 性能 | 组件级更新 | 节点级/动态节点级更新 |
| 微任务 | 使用 microtask 异步更新队列 | 同上(优化了调度机制) |
| 自定义渲染器 | 需修改源码 | 支持 createRenderer 自定义渲染器 |
| React Native / 小程序 | Weex | 通过自定义渲染器支持更广泛 |
新特性详解
Teleport
<Teleport> 允许将模板的一部分"传送"到 DOM 的其他位置,常用于模态框、通知、弹窗等需要脱离当前组件层叠上下文的场景。
<template>
<button @click="openModal">打开模态框</button>
<!-- 将模态框渲染到 body 下 -->
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal-content">
<slot />
</div>
</div>
</Teleport>
</template>to 属性接收 CSS 选择器字符串或 DOM 元素引用。多个 <Teleport> 可以挂载到同一目标,按顺序排列。
Suspense
<Suspense> 用于处理异步依赖(异步组件、async setup、Suspense 边界内的异步操作),提供 pending/fallback 状态管理。
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>Suspense 在 3.3+ 版本中标记为稳定特性,其核心机制是等待内部所有异步操作完成后,再渲染默认内容。
Fragments
Vue 3 支持组件模板拥有多个根节点(Fragment),避免了 Vue 2 中需要额外包裹元素的限制。
<!-- Vue 3 - 合法 -->
<template>
<header>标题</header>
<main>内容</main>
<footer>底部</footer>
</template>编译时,Vue 3 为多根模板自动创建一个 Fragment VNode,patchChildren 会使用专门的 Fragment 更新逻辑。
其他新特性
| 特性 | 说明 |
|---|---|
defineComponent | 类型推断辅助函数,纯 TypeScript 增强 |
defineAsyncComponent | 声明式异步组件 |
emits 选项 | 声明组件触发的事件,支持校验 |
多 v-model 绑定 | v-model:title、v-model:content 等 |
composable | 基于函数的逻辑复用模式(替代 mixin) |
<script setup> | 编译时语法糖,减少样板代码 |
CSS v-bind | 在 <style> 中绑定响应式数据 |
与 React 的响应式方案对比
| 对比维度 | Vue 3 (Proxy) | React (Hooks) |
|---|---|---|
| 侦测机制 | 运行时自动追踪依赖 | 手动声明依赖(useEffect 依赖数组) |
| 依赖粒度 | 属性级别自动追踪 | 整个 useEffect 回调 |
| 重新执行时机 | 依赖变化自动触发 | 依赖变化 + 组件重新渲染时 |
| 数据可变性 | 响应式数据直接修改(可变) | 需要不可变更新(setState 创建新对象) |
| 异步依赖追踪 | watchEffect 在同步代码中自动收集,异步中丢失 | 所有依赖需显式声明 |
| 上下文渲染 | 组件级自动追踪 | 状态变化触发整个组件重渲染 |
| 调试难度 | devtools + onRenderTracked / onRenderTriggered | React DevTools + useEffect 检查 |
| 循环依赖处理 | 内置检测机制 | 需要开发者小心处理 |
| 学习曲线 | 声明式响应式,偏向"魔法" | 函数式 + 不可变,更接近 JS 语义 |
React 的响应式局限举例:
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
// 闭包陷阱:count 永远是 0
setCount(count + 1)
}, 1000)
return () => clearInterval(timer)
}, []) // 依赖数组为空
}Vue 3 的等价写法:
const count = ref(0)
watchEffect(() => {
// 自动追踪 count,始终拿到最新值
const timer = setInterval(() => {
count.value++
}, 1000)
onUnmounted(() => clearInterval(timer))
})核心理念差异:
- Vue:数据驱动视图,修改状态 → 自动更新视图。响应式系统自动维护依赖关系图,开发者不需要关心"何时更新"。
- React:状态决定视图,
setState→ 重渲染整个组件。依赖管理是开发者显式控制的,提供了useEffect、useMemo、useCallback等工具。
两种方案各有优劣:Vue 的自动依赖追踪更为直观且不易出错(对新手友好),React 的显式依赖管理更可预测且易于静态分析(利于编译器优化)。
总结
Vue 3 通过将响应式系统从 Object.defineProperty 迁移到 Proxy,从根本上解决了 Vue 2 在数组和新增属性上的能力局限。结合 Composition API 提供的灵活逻辑组织能力、编译阶段的 PatchFlag / Block Tree / 静态提升等优化手段,Vue 3 在现代前端框架中同时兼顾了开发体验与运行时性能。
其响应式系统的精巧设计——从 reactive / ref 的创建,到 track / trigger 的依赖收集与派发,再到 effect 的调度执行——构成了一套完整且高效的响应式编程模型。理解这套机制有助于在实际开发中写出更高效、更可预测的 Vue 3 应用。
交互式 Demo
以下 Demo 直观展示了 Vue 2(Object.defineProperty)与 Vue 3(Proxy)在响应式能力上的差异,包括数组索引、新增属性等关键场景的对比: