彻底告别 `$set`:从“数字化餐厅”看 Vue3 响应式的全感知进化

Unknown Author 独酌
2026-01-30 前端

在前端面试的深水区,Vue3 的响应式系统是衡量一个开发者是否触及框架灵魂的试金石。很多人知道 Vue3 用了 Proxy 代替了 Object.defineProperty,但其背后的底层代差和调度艺术,远不止“支持数组”那么简单。

今天,我们把 Vue3 响应式系统搬进一间 “数字化智慧餐厅”,带你深度拆解其底层的进化逻辑。


一、 角色分配:响应式系统的“数字化”图谱

为了方便理解,我们将源码中的抽象概念对应到餐厅的各个角色中:

源码概念 餐厅角色 职责描述
Proxy (代理) 智能电子菜单/平板 顾客(代码)看菜、点菜、改单都要经过它,它能感知一切。
Track (依赖收集) 点餐意向登记 记录哪个桌号(Effect)关注了哪道菜(Key)。
Trigger (触发更新) 出餐广播/后厨通知 某道菜变动了,按登记表通知对应的桌号。
Scheduler (调度器) 传菜升降梯 即使后厨瞬间出了10道菜,也先放进梯子,等攒够了/忙完了再统一送餐。

二、 维度打击:为什么 Proxy 是“降维打击”?

1. 从“纸质表单”到“多点触控屏”

Vue2 的 Object.defineProperty 就像是纸质表单

  • 局限:你必须预先在表单上印好“菜名”方格。如果你想在空白处加个“备注”(新增属性),或者抹掉一个菜(删除属性),扫描仪(Vue2)是感知不到的,因为它的监控点是死板且固定的。
  • 代价:这导致 Vue2 初始化时必须递归遍历对象所有属性。如果菜单有 1000 道菜,开门加载速度会极慢。

2. 惰性监听(Lazy Conversion)

Vue3 的 Proxy 是按需服务
当你没看某道嵌套很深的菜时,Proxy 根本不会去理它。只有当你指着那道菜问“这个多少钱?”(访问属性)时,Proxy 才会即时对该层级进行响应式转换。这种“用到才代理”的策略,让 Vue3 在处理海量数据时,首屏启动速度远超 Vue2。


三、 深度解析:Proxy 的 13 种拦截“传感器”

Proxy 不仅仅能监听 getset,它布满了 13 种传感器,实现了**从“属性劫持”到“全行为代理”**的跃迁。

  • get / set (基础交互):最基本的查看和修改。
  • has (in 操作符):当代码执行 'price' in dish 时,Vue3 能感知到你的查询意图。
  • deleteProperty (delete):当你删掉一个属性,Vue3 立即触发更新,不再需要手动打补丁。
  • ownKeys (遍历 - Object.keys):当你在看“今日推荐列表”时,Vue3 记下了你对“整个列表结构”的依赖。

代码演示:如何感知遍历?

1
2
3
4
5
6
7
8
9
10
11
// Vue3 内部模型简述
const dish = reactive({ name: '红烧肉', price: 58 });

// 副作用函数:关注了菜单的“所有键名”
effect(() => {
console.log("菜单更新了:", Object.keys(dish));
});

// 当我们新增一个属性(Vue2 做不到,Vue3 通过 set 拦截并触发 ownKeys 依赖)
dish.category = '热菜';
// 自动触发打印:菜单更新了:['name', 'price', 'category']

四、 核心闭环:Track 追踪与 Trigger 触发

Vue3 内部通过一套严密的内存映射表(targetMap)来管理成千上万的“点餐需求”。

1. Track (依赖收集)

effect 执行并读取数据时,触发 Proxy 的 get,调用 track

  • 内存模型WeakMap<target, Map<key, Set<effect>>>
  • 意图:它在后台记下:“1号桌(Effect)看中了‘红烧肉’(Key)”。

2. Trigger (触发更新)

当数据被修改时,触发 Proxy 的 set,调用 trigger

  • 意图:它翻开记录本,找到所有关注“红烧肉”的桌号,依次通知他们执行更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 极简实现参考
function track(target, key) {
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()));
dep.add(activeEffect); // 登记当前正在运行的“桌号”
}

function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
dep.forEach(effect => {
if (effect.scheduler) effect.scheduler(); // 如果有调度器,走调度流程
else effect.run(); // 否则直接更新
});
}

五、 调度器(Scheduler):传菜升降梯的艺术

如果后厨一秒钟内对“红烧肉”做了 10 次微调(改色、加盐、撒葱…),餐厅难道要给客人送 10 次菜吗?

Scheduler 的核心作用:异步批处理 (Batching)

  1. 缓存任务:当数据变化触发 trigger 时,更新任务不会立即执行,而是被塞进一个微任务队列(像是在升降梯里攒着)。
  2. 去重优化:如果同一个组件触发了 100 次更新,队列里只会保留一个任务。
  3. 最终送达:等当前所有的同步代码执行完毕,调度器会通过 Promise.resolve() 启动微任务,一次性把最终的菜品(最终状态)送到顾客桌上。

这套机制保证了:无论你多么频繁地操作响应式数据,DOM 的更新永远是丝滑且最高效的。


六、 结语

Vue3 的响应式系统不仅仅是将 defineProperty 换成了 Proxy

它是一套 从“补丁式监听”进化到“全感知代理” 的架构,通过 WeakMap 解决了内存泄漏隐患,通过 惰性转换 提升了首屏性能,通过 Scheduler 实现了精准的异步调度。

理解了这些,你也就读懂了 Vue3 为何能在大规模复杂应用中,依然保持轻量且强大的底层逻辑。

独酌

独酌

小镇码农

相关文章