Vue 3 的响应式系统是一次彻底的重写。相比 Vue 2 基于 Object.defineProperty 的实现,Vue 3 采用 ES6 的 Proxy 作为核心,在性能、能力扩展性和代码可维护性上都有显著提升。本文将从底层机制入手,逐步拆解 Vue 3 响应式的工作原理。
一、为什么放弃 Object.defineProperty?
Vue 2 的响应式通过 Object.defineProperty 对对象属性的 get 和 set 进行劫持,存在几个固有限制:
- 无法监测属性新增/删除:defineProperty 只能劫持已存在的属性,新增属性需要手动调用 Vue.set。
- 无法监测数组索引和 length 变化:需要重写数组原型方法来 hack。
- 深层监听需要一次性递归:初始化时遍历整个对象,对大型对象性能不友好。
Proxy 则直接代理整个对象,拦截包括属性读取、设置、删除、遍历在内的 13 种操作,从根本上解决了上述问题。## 二、reactive 的核心实现
Vue 3 中 reactive 函数的本质是创建一个 Proxy:
javascript
javascript
function reactive(target) {
// 如果已经是只读对象,直接返回
if (isReadonly(target)) return target
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
核心的拦截逻辑在 mutableHandlers 中,简化后大致如下:
javascript
javascript
const mutableHandlers = {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 依赖收集
track(target, 'get', key)
值得注意的两个细节:
- 懒代理(Lazy Reactive) :深层响应式并不是在初始化时递归遍历,而是在访问到某个属性时才判断它是否是对象,是的话再代理一次。这避免了初始化时的性能开销。
- Reflect.set 配合 receiver:保证子对象继承属性的 this 指向正确,避免原型链上的 setter 异常。
三、依赖收集:track 与 effect
响应式系统的本质是建立「数据 → 副作用」的映射关系。Vue 3 用一个全局变量 activeEffect 来记录当前正在执行的副作用函数,配合 WeakMap → Map → Set 的三层结构存储依赖:
javascript
txt
targetMap (WeakMap)
└── target → depsMap (Map)
└── key → dep (Set of effects)
track 的简化实现:
csharp
javascript
let activeEffect = null
function track(target, type, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
effect 函数的作用是执行副作用,并在执行前把自己设为 activeEffect:
scss
javascript
function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
这样当副作用函数内访问响应式数据时,就会触发 get → track,把当前 effect 加入到对应 key 的 dep 集合中。
四、触发更新:trigger
当数据被修改时,set 拦截器调用 trigger,遍历对应 key 下收集到的所有 effect 并执行:
vbnet
javascript
function trigger(target, type, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
这里有一个容易踩坑的点:必须拷贝 Set 后再遍历。否则 effect 执行过程中如果又改了数据,可能导致同一个 dep 集合一边迭代一边修改,出现无限循环。
五、ref:另一种响应式形态
ref 面向基本类型。因为 Proxy 只能代理对象,Vue 3 用一个带 value 属性的对象包装原始值:
javascript
javascript
function ref(value) {
return new RefImpl(value)
}
class RefImpl {
toReactive 的逻辑是:对象类型走 reactive,基本类型直接返回。这样 ref 既能包装基本类型,也能在传入对象时获得深层响应式。
六、computed 与 watch 的依赖关系
computed 本质上是一个带缓存能力的 effect,它内部维护 dirty 标志。当依赖的响应式数据变化时,trigger 会把 dirty 置为 true,下次访问 .value 时才重新计算。
watch 则是显式声明依赖,通过 effect + scheduler 实现:依赖变化时不立即执行副作用,而是调用 scheduler,把回调放入异步队列。
七、实践建议
- 避免在响应式对象上做频繁的全量替换,尤其是大型数组。替换会触发整棵树的重新代理。
- 深层只读用 readonly,深层响应用 reactive,基本类型用 ref,避免混用造成心智负担。
- shallowRef / shallowReactive 在只关心第一层变化的场景下能显著减少开销,比如大数据表格。
- 解构 reactive 会丢失响应式,需要用 toRefs 包裹;ref 解构则要用 unref 或 .value。
- 慎用 watchEffect,它的自动依赖收集虽方便,但在复杂异步逻辑中容易捕获到非预期依赖,建议在关键逻辑上用显式 watch。
总结
Vue 3 的响应式系统通过 Proxy + WeakMap 依赖树 + effect 调度,构建了一套既高效又可扩展的机制。理解 track/trigger 的本质、懒代理的设计意图,以及 ref 与 reactive 的边界,能帮助我们在实际项目中写出更可预测、性能更优的代码。
响应式不是魔法,它只是「数据访问 → 收集依赖;数据变更 → 触发依赖」这一朴素思想的工程化落地。掌握了这一点,很多看似诡异的 bug 都能迎刃而解。