Vue 3 彻底抛弃了 Vue 2 中基于 Object.defineProperty 的响应式方案,全面转向 Proxy ,同时引入了全新的 编译策略 与 静态提升 技术。这些底层变革不仅带来了更优的性能,更让 Vue 3 得以支持对 Map、Set 等数据类型的监听。本文将从源码层面,抽丝剥茧地分析其响应式核心 reactive 与 effect 的运行机制,以及编译器如何配合实现靶向更新。
一、响应式基石:从 reactive 说起
打开 Vue 3 的源码,reactive 函数位于 packages/reactivity/src/reactive.ts。它的本质是创建一个 Proxy 代理对象,拦截所有读写操作。
ts
scss
// 简化的 reactive 核心逻辑
function reactive(target) {
// 如果目标已经是一个代理,或为基本类型,直接返回
if (isProxy(target) || !isObject(target)) return target
// 检查缓存,避免重复代理同一个对象
const existingProxy = reactiveMap.get(target)
if (existingProxy) return existingProxy
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
const res = Reflect.get(target, key, receiver)
// 如果值是对象,则递归进行深层代理(惰性代理)
if (isObject(res)) return reactive(res)
return res
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 值发生变化时触发更新
if (hasChanged(value, oldValue)) {
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, key)
}
return result
}
})
reactiveMap.set(target, proxy)
return proxy
}
这里的核心设计有三点:
- 惰性深层代理 :只有在
get中访问到深层属性时,才会对子对象调用reactive,而非初始化时就递归遍历整个对象。这极大减少了初始化的性能开销。 - 依赖收集
track:所有在effect上下文中被读取的响应式数据,都会与当前活跃的effect函数建立映射关系。 - 派发更新
trigger:当数据被修改或删除时,找到所有依赖它的effect并重新执行。
二、依赖收集与追踪的魔法:track 与 targetMap
依赖收集的核心数据结构是一个三层 Map:
text
javascript
WeakMap<target, Map<key, Set<ReactiveEffect>>>
源码中用 targetMap 表示。当一个响应式对象的属性被读取时,track 函数会找到该对象对应的 depsMap,再找到该属性对应的 dep(一个 Set),最后把当前正在运行的 activeEffect 加入这个 Set 中。
ts
scss
// 简化的 track 实现
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
// 同时让 effect 记住这个 dep,用于后续清理
activeEffect.deps.push(dep)
}
}
activeEffect 是一个全局变量,指向当前正在执行的副作用函数。当组件渲染函数或 watchEffect 回调运行时,它会被临时设置为这个回调对应的 ReactiveEffect 对象。执行完毕后恢复为之前的 effect,这就构成了一个嵌套 effect 栈。
trigger 的逻辑则是逆过程:从 targetMap 中找到 depsMap,再根据 key 找到 dep,遍历其中的所有 effect 并执行。对于数组的长度变更或 for...in 遍历等特殊情况,还会触发额外的 ITERATE_KEY 相关依赖。
三、副作用调度器:effect 与异步更新队列
ts
kotlin
// 简化的 effect 函数
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run()
const runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn
this.scheduler = scheduler
this.deps = [] // 记录该 effect 被哪些 dep 收集
this.active = true
}
run() {
if (!this.active) return this.fn()
try {
this.parent = activeEffect
activeEffect = this
cleanupEffect(this) // 每次运行前清除旧的依赖关系
return this.fn()
} finally {
activeEffect = this.parent
this.parent = undefined
}
}
}
当组件渲染时,Vue 内部会创建一个组件更新 effect,并传入一个 scheduler。这个 scheduler 不是立即执行 fn,而是将更新任务推入一个微任务队列:
ts
scss
const queue = []
let isFlushing = false
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(flushJobs)
}
}
这样,在同一轮事件循环中对响应式数据的多次修改,只会触发一次组件更新。这就是 Vue 3 著名的异步批量更新机制。
四、编译器的静态提升与 Block Tree
仅仅依靠响应式系统,每次数据变更都要重新生成虚拟 DOM 树并执行全量 diff,仍然存在性能瓶颈。Vue 3 的编译器在模板编译阶段做了大量优化,其中最关键的是静态提升 和Block Tree。
模板 <div><span>hello</span><p>{{ msg }}</p></div> 经过编译后,大致生成如下渲染函数:
js
javascript
// 静态节点被提升到外部,不再重复创建
const _hoisted_1 = /*#__PURE__*/ createVNode("span", null, "hello")
function render(ctx, cache) {
return (openBlock(), createBlock("div", null, [
_hoisted_1,
createVNode("p", null, ctx.msg)
]))
}
openBlock 与 createBlock 引入了一个关键概念:动态节点收集 。Block 节点会记录其内部所有动态子孙节点(包含 patchFlag 标记的节点),形成一个扁平化的动态节点数组。在更新时,只需遍历这个数组进行靶向 diff,而无需遍历整棵静态树。
patchFlag 是一个位掩码,例如:
1代表文本动态2代表 class 动态8代表 props 动态
Diff 阶段,遇到带有 patchFlag 的节点,会直接进入优化的路径:
ts
scss
// 简化版 patchElement
const patchElement = (n1, n2) => {
const el = (n2.el = n1.el)
const patchFlag = n2.patchFlag
if (patchFlag & PatchFlags.TEXT) {
// 只更新文本内容,跳过 props 比较
if (n1.children !== n2.children) {
setElementText(el, n2.children)
}
} else if (patchFlag & PatchFlags.CLASS) {
// 仅处理 class 变化
hostPatchProp(el, 'class', null, n2.props.class)
} else {
// 全量 props diff
patchProps(el, n2, n1.props, n2.props)
}
// 递归处理子节点(静态子节点会被跳过)
if (n2.dynamicChildren) {
patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren, el)
} else {
patchChildren(n1, n2, el)
}
}
dynamicChildren 正是 block 中收集的动态节点数组,它使得更新可以跳过静态内容,直接命中可能变化的节点。
五、靶向更新是如何工作的
假设有如下模板:
html
css
<div>
<span>静态文本</span>
<span>{{ dynamic }}</span>
</div>
编译后的结果会创建一个 block,其 dynamicChildren 只包含那个含有动态文本的 span。当 dynamic 变化时,响应式系统触发组件更新 effect,组件执行渲染函数,由于 block 的存在,新的 VNode 会再次收集动态子节点。接着进入 patchElement,检测到 n2.dynamicChildren 存在,直接比对两个动态子节点数组。因为只有一条动态数据,diff 过程几乎是 O(1) 级别,完全绕过了对静态 span 的访问。
这种设计使 Vue 3 在大规模静态模板中获得了接近原生 JavaScript 的手动优化性能。
六、总结
Vue 3 的底层革新远不止从 Object.defineProperty 换为 Proxy 这么简单。它以 Proxy 为地基,构建了精准的依赖追踪与多层级调度系统,再通过编译阶段的静态提升与 Block Tree,实现渲染层的靶向更新。响应式系统和编译优化的深度协同,让 Vue 3 在保持开发友好性的同时,将运行时性能推向了新的高度。对于框架使用者而言,理解这些底层机制不仅能帮助我们写出更高效的代码,也能在业务遭遇性能瓶颈时,多一分调试与优化的底气。