Vue2 验证了「响应式驱动视图」的威力,却在数组索引、属性增删等场景留下无法追踪的死角。Vue3 并未缝缝补补,而是把整座大厦的地基------数据拦截机制------彻底替换为 Proxy。本文沿着「拦截 → 创建 → 收集」三条链路,带你读懂这一次底层跃迁的全部细节。
一、拦截:从点到面的语义升级
1.Vue2 的「定点拦截」
Object.defineProperty
只能劫持已存在的属性。当业务代码 obj.newKey = 1
时,这条新增路径对依赖系统完全不可见,于是官方只能额外暴露 Vue.set / vm.$set
作为补丁。
2.Vue3 的「整面拦截」
Proxy 把「对象」视为一个整体,任何对属性的 读取、写入、删除、遍历 乃至 原型链读取 都能被捕获。
ts
const p = new Proxy(obj, {
get(target, key, receiver) { /* 读 */ },
set(target, key, value, receiver) { /* 写 */ },
deleteProperty(target, key) { /* 删 */ }
})
新增 key 不再逃逸,数组索引与 length 的变化自然落入监听范围,彻底告别 $set
。
二、创建:ref 与 reactive 的分工
虽然 Proxy 能力更强,但「原始值」无法被代理。Vue3 用 RefImpl 与 ReactiveImpl 两套实现互补:
- ref 负责原始值(Number、String、Boolean)------内部用
RefImpl
包裹,读/写时触发自定义 getter/setter;当值是对象时再递归交给reactive
。 - reactive 负责对象/数组------直接返回 Proxy,拦截全部操作。
源码片段(精简):
ts
class RefImpl<T> {
get value() {
track(this, 'value') // 收集
return this._value
}
set value(newVal) {
this._value = toReactive(newVal)
trigger(this, 'value') // 派发
}
}
function reactive(target: object) {
return createReactiveObject(target, mutableHandlers)
}
toReactive
判断传入值是否为对象,是则继续包 Proxy,否则原样返回,形成一条「层层代理,按需终止」的链。
三、收集:从 Watcher 树到副作用图
1.Vue2 的 Watcher + Dep
每个响应式属性拥有一个 Dep,Dep 里存着若干 Watcher(通常是一个组件渲染函数)。属性变化 → 通知所有 Watcher → 组件级重渲染。
粒度:组件级别。
2.Vue3 的副作用图
Vue3 不再关心「是哪个组件」,而是关心「哪个副作用函数」。数据结构是一张 WeakMap → Map → Set 的三级表:
- 第一级 WeakMap:键是响应式对象,值是第二级 Map;
- 第二级 Map:键是属性名,值是第三级 Set;
- 第三级 Set:存储所有依赖该属性的
effect
函数。
当属性值改变时,只触发 精确到函数 的重新执行,粒度从组件级降到函数级,更新范围更小,性能更高。
四、工程实践的迁移
- Vue2 项目:若观察到大量
this.$set
或Vue.set
,说明已踩中响应式盲区,升级 Vue3 可一次性消除。 - 新 Vue3 项目:优先使用
reactive
管理对象,ref
管理原始值;避免把reactive
包进ref
,防止双重代理带来的额外开销。 - 性能调优:借助
markRaw
或shallowReactive
把大列表、第三方库实例标记为非响应式,减少追踪压力。
结语
Vue3 的响应式不是「打补丁」,而是「换引擎」。Proxy 让「增删改查」全部可追踪,WeakMap 让依赖收集更精准。这次底层跃迁,彻底解决了 Vue2 的响应式难题,也为 Vue3 的性能优化奠定了基础。