深入理解 Vue 3 响应式原理:从 Proxy 到依赖收集

Vue 3 的响应式系统是一次彻底的重写。相比 Vue 2 基于 Object.defineProperty 的实现,Vue 3 采用 ES6 的 Proxy 作为核心,在性能、能力扩展性和代码可维护性上都有显著提升。本文将从底层机制入手,逐步拆解 Vue 3 响应式的工作原理。

一、为什么放弃 Object.defineProperty?

Vue 2 的响应式通过 Object.defineProperty 对对象属性的 get 和 set 进行劫持,存在几个固有限制:

  1. 无法监测属性新增/删除:defineProperty 只能劫持已存在的属性,新增属性需要手动调用 Vue.set。
  2. 无法监测数组索引和 length 变化:需要重写数组原型方法来 hack。
  3. 深层监听需要一次性递归:初始化时遍历整个对象,对大型对象性能不友好。

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,把回调放入异步队列。

七、实践建议

  1. 避免在响应式对象上做频繁的全量替换,尤其是大型数组。替换会触发整棵树的重新代理。
  2. 深层只读用 readonly,深层响应用 reactive,基本类型用 ref,避免混用造成心智负担。
  3. shallowRef / shallowReactive 在只关心第一层变化的场景下能显著减少开销,比如大数据表格。
  4. 解构 reactive 会丢失响应式,需要用 toRefs 包裹;ref 解构则要用 unref 或 .value。
  5. 慎用 watchEffect,它的自动依赖收集虽方便,但在复杂异步逻辑中容易捕获到非预期依赖,建议在关键逻辑上用显式 watch。

总结

Vue 3 的响应式系统通过 Proxy + WeakMap 依赖树 + effect 调度,构建了一套既高效又可扩展的机制。理解 track/trigger 的本质、懒代理的设计意图,以及 ref 与 reactive 的边界,能帮助我们在实际项目中写出更可预测、性能更优的代码。

响应式不是魔法,它只是「数据访问 → 收集依赖;数据变更 → 触发依赖」这一朴素思想的工程化落地。掌握了这一点,很多看似诡异的 bug 都能迎刃而解。

相关推荐
晚安日记wanna1 小时前
Vue3 script setup 的四层追问答到第三层才算过关
前端·vue.js·面试
前端柱子1 小时前
Chaikin‘s Corner Cutting 算法 应用canvas绘制平滑的曲线
前端
gnip1 小时前
uts 插件示例:获取设备电量信息
前端·javascript
码上成长1 小时前
Mapbox 上用 Turf 裁多边形:屏幕贴边了,接口却说越界
前端·前端框架
计算机魔术师1 小时前
纽约时报告了OpenAI和微软:820万条聊天记录背后,是AI时代最大的版权保卫战
前端
码事漫谈2 小时前
在 Kubernetes 上管好数据库:金仓 KES-Operator 正式落地
前端·后端
用户1880687493522 小时前
手上全是油,我怎么让rokid眼镜在厨房里教我炒菜
前端
gnip2 小时前
uni-app 原生插件
前端·javascript·uni-app
gnip2 小时前
跨域常用解决方案
前端·javascript