想象你住在菜市场旁边,老板们有一块块“智能公示牌”——牌上写着当天菜价。你不需要反复跑来跑去问价,只要牌上的数字一变,牌上的小喇叭就会自动喊一嗓子,所有关注这个菜价的人都能第一时间知道。更神奇的是,这块牌不仅能“改动旧价签”,还能在角落新贴一张“新价签”、撕掉一张“过期价签”,全都逃不过它的眼睛。 Vue3 的响应式系统,本质上就是这一块块“智能公示牌”。它的底层秘密是一种叫 Proxy 的“智能感应膜”:只要有人改动数据,感应膜立刻感知,翻开随身携带的“小本本”查出谁在关注这个数据,再通过“大喇叭”通知这些人——前端术语叫“派发更新”。而 Composition API,则是把“买菜、砍价、装袋、记账”这些动作打包成一张张“流程卡”,让你能自由组合复用。 本文就是带你走近这套机制:从 Proxy 如何拦截所有动作,到 track 如何记录依赖、trigger 如何广播通知,再到 reactive、ref、computed 这些常用 API 背后的小秘密。学完你就能理解:为什么 Vue3 能感知属性增删、为什么解构会丢响应式、以及 effect 是在什么时候悄悄运行起来的。
对这个岗位的价值:前端工程师吃透响应式原理后,能看懂 Vue3 源码、游刃有余地解决状态管理难题,还能把 Proxy 这套“感知-记录-通知”思想迁移到可视化大屏、AI 监控面板等大量实时数据场景,是进阶资深前端的硬通货。
可视化演示
动态演示Vue3响应式原理的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Vue3响应式原理:VueP Vue3 Proxy响应式系统 ref与reactive响应式 核心机制可视化 Vue3响应式原理整体架构与关键组件
响应式初识
清晨的菜市场,摊主刚给“白菜”那块智能公示牌改了价:从 3 元变成 2.5 元。下一秒,正在挑菜的大爷手机里的“买菜清单”就同步变价了。这就是响应式的魔力——数据一变,依赖它的地方自动跟着变,不需要任何人去手动刷新。
Vue2 时代的做法像是“老式胶贴价签”:每块牌子上提前挖好固定的小格子,格子里面贴着数字标签。改价时得小心翼翼把旧标签撕下来,贴上新标签。可要是想新增一行“今日特惠”的价签?对不起,牌子上没留这个格子,得重新做一块牌子。
Vue3 则把整块牌子换成了“智能感应公示牌”。它不再关心牌子上预挖了几个格子,而是像蒙了一层感应膜,任何与牌子相关的动作都会被感应膜捕捉:改数字、贴新签、撕旧签、甚至有人只是“看了一眼”都能感知。这层感应膜,正是 Proxy。Vue3 的响应式系统由此打通了“数据 → 视图”的任督二脉:
读懂这一步,就等于拿到了理解 Vue3 响应式一切的“总钥匙”:后面所有细节——Proxy 怎么拦截、小本本怎么记、喇叭怎么喊——都只是对这两行流程的展开。
- 读取(get 拦截)任何代码读取响应式对象的属性时,Proxy 的 get 处理器会抢先执行,在把值交出去之前先做一次“登记”。
- track 收集get 处理器调用 track(),把当前正在运行的 effect 与这个属性绑定,相当于在小本本上写下“谁在看这块牌”。
- 写入(set 拦截)代码修改属性值时,set 处理器先把新值真正写入原对象,再触发后续通知动作。
- trigger 通知set 处理器调用 trigger(),取出该属性名下登记的所有 effect,挨个喊他们“菜价变了,该干活了”。
Proxy 拦截器
公示牌上的“智能感应膜”到底有多灵敏?它不仅看得见“改写数字”(set),看得见“有人读数字”(get),还看得见“贴一张新价签”(新增属性)和“揭掉一张旧价签”(删除属性)。在 JS 里,这张膜就是 new Proxy 创造出来的代理对象。
Proxy 接收两个参数:target 是被代理的原对象,就像被贴膜的“真实木板”;handler 是一组“钩子函数”,就像膜上连着的各种传感器。此后所有对代理对象的操作,都会先被膜“过滤”一遍:
先看概念形态,再看它如何替代 Vue2 的 Object.defineProperty。Vue2 的做法是“在固有格子上装弹簧秤”——初始化时遍历对象的每个已有属性,用 Object.defineProperty 把它们变成 getter/setter。这有两个先天缺陷:第一,对象后期新增的属性没装弹簧秤,完全不响应;第二,数组通过索引直接赋值也监听不到。而 Proxy 是对整个对象加膜,不管属性是已有还是新增、是对象还是数组元素,所有读写都绕不开膜。
有了这层膜,Vue3 响应式的大厦就有了地基:接下来的依赖收集和派发更新,全都是在 get 和 set 这两个钩子里做文章。
- target 原对象要加膜的“真实木板”,也就是业务里定义的数据本体,内容存这里,但外部拿到的不是它。
- handler 处理器膜上传感器的“接线说明书”,可以声明 get、set、deleteProperty 等钩子,分别对应读、改、删动作。
- proxy 代理对象对外暴露的“加了膜的牌子”,外部一切读写都走它;业务代码只跟它打交道,不碰 target。
- Reflect 同步操作在钩子内部通常配合 Reflect 完成真正的读写,保证 this 指向正确,返回值和语义都规矩。
依赖收集
大爷站在公示牌前看了三秒钟菜价,老板就在旁边的小本本上记了一笔:“张大妈关注白菜价”。这个小本本在 Vue3 里叫 Dep(依赖),每个响应式属性都配一个专属小本本,本子上写着所有“看过它”的 effect 函数。
小本本的设计很简单:每个属性对应一个 Dep 对象,内部用一个集合(Set)存储 effect。为什么用 Set?因为同一时刻一个 effect 可能重复读取同一属性多次,集合天然去重,保证“登记一次即可”。
当组件渲染时,渲染函数会读取多个响应式属性;每读一个属性,就把当前正在运行的渲染 effect 登记到对应属性的 Dep 里。这个过程叫 track(依赖收集)。做到这一点靠的是一个全局变量 activeEffect:effect 开始执行前把自己设为 activeEffect,执行期间所有读取到的属性都会把这个 effect 记进小本本。
收集完成之后,小本本上就有了一份清晰的“关注者名单”。将来菜价一改,老板只需要翻名单喊人,不用满市场乱找。
- 定位小本本track() 先根据 target 对象和 key 属性名,找到该属性对应的 Dep 依赖集合,没有则新建。
- 取出活动 effect读取全局 activeEffect,它就是当前正在运行的副作用函数,是“正在盯着价签的那个人”。
- 登记去重把 effect 添进 dep.subs 集合;Set 的天然性质让重复添加自动失效,同一属性的同一 effect 只会记一次。
派发更新
下午三点,白菜突然从 2.5 元涨到 3.5 元。老板翻出“白菜价”那本小本本,对着名单一个个喊:“张大妈,白菜涨价了!李大婶,你也关注了白菜!”每一位在名单上的 effect 都会重新执行一遍——有的去更新手机清单,有的去重算今日买菜预算。这就是 trigger(派发更新)。
但这里有个讲究:如果大爷同时关注了白菜、萝卜、土豆三块牌子,而三块牌子的价格在同一个瞬间被连续修改,老板难道要连续朝他喊三嗓子?太吵了。Vue3 的解决办法是“先记到今天的待办小黑板上,傍晚统一广播一次”。这个待办黑板就是任务队列:trigger 时不立即执行 effect,而是把 effect 丢进队列;如果队列里已经有这个 effect,就不再重复添加;等本轮同步代码跑完,再用微任务统一 flush 队列。
正是这一步“统一广播”的设计,让 Vue3 面对频繁修改数据时依然游刃有余——这正是后面“性能优化”一节的伏笔。
- 取名单根据 key 找到属性对应的 dep.subs,拿到所有关注者的 effect 集合。
- 入队去重逐个把 effect 放入调度队列;队列用 Set 或带标记的数组实现,已入队的 effect 不会二次入队。
- 异步 flush当前同步任务结束后,在微任务阶段一次性清空并执行队列里所有 effect,合并多次修改为一次更新。
响应式API
公示牌有了,感应膜有了,小本本和大喇叭也都有了,Vue3 把这些零件组装成几个常用 API 摆在开发者的工具箱里:
- reactive(obj):给整个对象加膜,适合多字段状态。相当于“整块多行智能公示牌”。
- ref(value):给单个基本值加膜,内部包装成
{ value: 原始值 }再走响应式。相当于“单格手写牌”,格子里的数字会动。
- computed(fn):自动算价的电子秤——依赖的各路菜价一变,秤上显示的总价自动重算,而且还带缓存。
- toRefs(obj):把 reactive 的每个属性变成独立 ref 返回,专门解决“解构后丢响应式”的坑。
为什么直接解构 reactive 对象会丢响应式?因为解构拿到的是“原始木板上的数字”而不是“感应膜后的代理对象”。toRefs 相当于把每一格都单独再做一块小感应牌,解构出来的每个变量依然连着感应膜。
这些 API 单独用都很简单,但当它们组合进 Composition API 的逻辑组织里时,才会释放真正的生产力。
- reactive 加膜把普通对象丢进 new Proxy,返回带有响应式能力的代理对象,业务侧读写都经过 get/set 钩子。
- ref 包装把基本值塞进
{ value: v }对象,再对它做 reactive;这样 .value 属性天然变成响应式的。 - computed 缓存computed 内部维护一个“脏值标记”,依赖改变才重算,否则直接返回缓存值。
- toRefs 还原对 reactive 对象每个 key 创建一个 ref 并指向原对象属性,解构后每个变量仍是响应式引用。
CompositionAPI
以前在 Vue2 里,一个组件的数据、方法、计算属性、监听器各自放在 options 的固定格子里,就像“把买菜流程拆成四本手册:买什么、怎么挑、付多少钱、记不记账”,谁看都得来回翻。Composition API 则给了你一张空白的“流程卡”,你可以把买菜的完整流程写成一段连续的文字,想怎么编排就怎么编排。
setup 是组件逻辑的入口,在组件实例创建后、正式渲染前执行。你在这里用 ref、reactive 声明响应式数据,用普通函数组织业务方法,用 computed 定义衍生状态,最后把需要暴露给模板的东西 return 出去。一段“买菜流程”就这样完整地封装进一个组合函数 useBuyVegetables(),任何组件都能拿来复用。
Composition API 本质上是把“响应式能力”与“组织代码的自由”缝合在一起:底层的 Proxy + track + trigger 机制完全不变,但开发者拿到的是一张灵活的白纸,而不是被划分好的格子。
- setup 时机组件实例创建后、首次渲染前,props 已就绪,此处可以安全读取 props 并初始化状态。
- 声明响应式数据在 setup 内用 ref/reactive 创建状态,它们会被接入全局响应式网络。
- 返回模板绑定把需要渲染的数据和方法放进返回值对象,模板编译时自动解包 ref。
性能优化
菜市场每天上午九点是涨价高峰,如果每块牌子每秒钟改十次价,老板要是不停地喊,嗓子早哑了。Vue3 的优化思路也正来自生活经验:攒一波,统一喊。
第一招是异步批处理:如第四节所说,任何一次 set 都只把 effect 丢进队列而非立即执行,多个属性变化最终只触发一次 flush。第二招是精确的依赖追踪:每个属性都有独立的小本本,改白菜价绝不会喊到只关心土豆价的人。第三招是惰性计算:computed 作为“电子秤”,只有在真正读数时才重新称重,平时直接显示缓存的总价。
这三招合在一起,让 Vue3 在大型应用里既“少干活”又“精准干活”。所谓“少干活”,是合并重复任务;“精准干活”,是只通知该通知的人。
性能优化不是炫技,而是让响应式系统在真实应用的大数据量、高频交互下依然保持流畅的呼吸感。
- 队列去重同一 effect 在一次更新周期内多次触发只入队一次,避免无谓重复执行。
- 异步统一刷新等当前调用栈清空、进入微任务时机后再 flush,把同步代码块里的密集修改合并成一次渲染。
- 精准依赖依赖按 key 精确划分,trigger 只遍历该 key 的 Dep,不打扰无关的 effect。
源码实战
所有零件都认识了一遍,现在把它们拼成一条完整的流水线。一个响应式系统的命脉,可以用一个循环来概括:
任何响应式应用都跑在这个循环里:effect 首次执行会读取若干属性 → get 钩子触发 track,把 effect 登记到这些属性的 Dep 里 → 之后任何写入触发 set 钩子 → trigger 从 Dep 取出 effect 入队 → 队列 flush 时 effect 重新执行 → 新一轮 track 更新依赖。
建议读者打开 Vue3 源码仓库,依次找到 packages/reactivity/src/effect.ts 里的 track、trigger 和 reactive.ts 里的 createReactiveObject——你会发现,真实源码就是这一条循环的工程化加厚。读懂了循环,你就读懂了 Vue3 响应式的灵魂。
- 注册 effect用 effect() 注册一个副作用函数,它立即执行一次,开启整个循环。
- 读取触发 track运行 fn 时读取响应式属性,get 钩子调用 track,把 fn 与属性绑定。
- 写入触发 trigger之后某处修改属性,set 钩子调用 trigger,取出 Dep 里的 effect 入队。
- 执行回到起点队列 flush 执行 effect,fn 再次读取属性并刷新依赖关系,循环闭合。
AI 应用场景
实时数据大屏看板
阶段:前端数据可视化与实时渲染
解决的问题:传统大屏每秒钟轮询接口再手动操作 DOM,数据多了会卡顿,逻辑也分散。Vue3 的响应式系统让后端推送的数据与前端图表自动同步:数据一变,图表组件重算、DOM 自动更新,开发者只需维护一个 reactive 状态对象。
具体案例:某智慧城市大屏展示全市菜市场实时菜价,后端每秒推送一次价格流。前端把这些价目丢进 reactive 对象,图表库(如 ECharts)的 option 用 computed 派生,数据源变了图表自动刷新,无需任何手动调用。
AI训练监控面板
阶段:模型训练过程监控与可视化
解决的问题:训练模型时 loss、acc、lr 等指标高频变化,传统写法要么频繁 setData 卡界面,要么手动节流。用 Vue3 的 ref 存指标、effect 驱动曲线渲染,配合异步批处理天然合并高频更新,面板流畅不抖动。
具体案例:训练一个图像分类模型,每 0.5 秒回传一次 loss。面板将 loss = ref(0),训练循环里不断修改 loss.value,折线图组件通过 computed 自动重算曲线数据——训练过程实时呈现,且不阻塞其他交互。
多轮对话状态管理
阶段:AI Agent / 大模型应用的前端会话状态
解决的问题:多轮对话涉及消息列表、正在输入状态、token 用量、上下文切换等大量状态,互相牵连。用 reactive 集中管理会话对象,任何一处的字段变化(新消息、输入状态翻转)都会自动同步到 UI 和关联逻辑。
具体案例:一个 AI 客服 Agent 的聊天界面,用 reactive({ messages: [], streaming: false, usage: { tokens: 0 } }) 保存会话状态;流式返回时逐字追加 messages 末尾的 content,界面自动滚动更新,助手“正在输入”的动画由 streaming 字段驱动。
Python 演示
# -*- coding: utf-8 -*-"""迷你 Vue3 响应式系统:用类属性拦截模拟 Proxy 的 track / trigger""" active_effect = None # 当前正在运行的 effect class Dep: def __init__(self): self.subs = set() # 关注该属性的 effect 集合 def depend(self): if active_effect is None: return self.subs.add(active_effect) def notify(self): for sub in list(self.subs): sub() class Reactive: def __init__(self, raw): object.__setattr__(self, '_raw', raw) object.__setattr__(self, '_deps', {}) def __getattribute__(self, key): if key.startswith('_'): return object.__getattribute__(self, key) deps = object.__getattribute__(self, '_deps') dep = deps.setdefault(key, Dep()) dep.depend() # 读取时收集依赖 raw = object.__getattribute__(self, '_raw') return raw[key] def __setattr__(self, key, value): if key.startswith('_'): object.__setattr__(self, key, value) return deps = object.__getattribute__(self, '_deps') raw = object.__getattribute__(self, '_raw') raw[key] = value dep = deps.get(key) if dep is not None: dep.notify() # 写入时派发更新 def __delattr__(self, key): if key.startswith('_'): object.__delattr__(self, key) return deps = object.__getattribute__(self, '_deps') raw = object.__getattribute__(self, '_raw') raw.pop(key, None) dep = deps.pop(key, None) if dep is not None: dep.notify() def effect(fn): """注册 effect:立即执行一次,触发读取并收集依赖""" global active_effect active_effect = fn try: fn() finally: active_effect = None return fn def main(): menu = Reactive({'fish': 8, 'pork': 15}) price_log = [] def watch_price(): price_log.append(menu.fish) effect(watch_price) # 首次执行,读取 fish 并收集依赖 menu.fish = 10 # 修改 fish,触发 watch_price menu.pork = 16 # watch_price 没读 pork,所以不触发 print(price_log) # [8, 10] if __name__ == '__main__': main()
active_effect = None全局“当前正在盯牌子的人”,effect 运行的瞬间才非空,用来标记谁该被登记。class Dep:每个属性的“小本本”:subs 集合存关注者,depend 登记,notify 喊人。dep.depend()在__getattribute__中调用,对应 Vue3 get 钩子里的 track 流程。dep.notify()在__setattr__中调用,对应 Vue3 set 钩子里的 trigger 流程。object.__setattr__(self, '_raw', raw)绕过普通 setattr 路径存内部数据,避免自己拦截自己形成死循环。key.startswith('_')内部属性名加下划线前缀,与业务字段隔离,这也是 Vue 源码保底 key 的常见约定。dep = deps.setdefault(key, Dep())首次读某个属性时新建小本本,之后复用,正是“按需建账”的思想。effect(watch_price)注册副作用并立即执行一次,触发menu.fish的读取,完成首次依赖收集。menu.fish = 10写入经过__setattr__拦截,notify 执行 watch_price,price_log 追加新价格。menu.pork = 16watch_price 从不读 pork,所以 pork 的 Dep 为空,修改它不会惊动任何人。
常见误区
误区以为用了 Proxy 之后,读写操作都发生在 Proxy 内部,原对象只是摆设,甚至可以把原对象删掉。
正解Proxy 只是“感应膜”,不是“存储柜”。真正的数据永远存放在 target 原对象里,Proxy 的 get/set 钩子只负责“拦截动作 + 登记通知”,最终读写还是要通过 Reflect 落到 target 上。就像感应膜能感知有人改价签,但价签本身还是贴在木板上。
为何常见:因为业务代码只见 Proxy 不见 target,容易误以为 Proxy 自带数据存储能力。
误区很多人写 `const { price } = reactive({ price: 8 })` 后,发现 price 不再响应,一头雾水,以为是 Vue3 的 bug。
正解解构拿到的是原始值,等于把“公示牌上的数字”直接抄到自己的小纸条上,纸条当然不会跟随公示牌变化。要保留响应式,必须用 toRefs(state) 把每个属性变成独立的 ref,解构出来的变量才连着感应膜。
为何常见:对象解构在 JS 里太自然了,容易忘记 reactive 返回的是 Proxy 代理对象,解构本质上是把内部原始属性值取了出来。
误区以为 effect 注册后处于待机状态,只有数据变了才会执行。
正解effect 在注册时会立即执行一次!正是这次执行过程中读取了哪些属性,才决定了它会被记进哪些 Dep 小本本。Vue3 的渲染 effect 首次执行就是首次渲染,之后的更新都是后续 trigger 驱动的。
为何常见:从“监听器”的直觉出发,容易把 effect 误解为“被动等待通知”的角色,忽略了它是通过主动执行来收集依赖的。
误区认为 Proxy 方案在性能上全面碾压 Object.defineProperty,Vue3 一定更“快”。
正解Vue3 的核心优势是能力增强(支持属性增删、数组索引赋值等),性能提升主要来自批量异步更新、更精确的依赖追踪和编译优化。Proxy 本身并不保证更快,如果滥用响应式(比如把超大对象整个做成 reactive),反而可能更慢。
为何常见:营销文章常强调“Proxy 更强大”,读者容易把“能力更强”偷换成“性能更快”。
面试题
Q1. Vue3 响应式系统的核心底层是基于哪个 JavaScript API 实现的?
- A.Object.defineProperty
- B.Proxy← 正确答案
- C.EventEmitter
- D.MutationObserver
答案:B。Vue3 用 Proxy 给对象加“感应膜”,能拦截新增、删除等所有动作;Object.defineProperty 是 Vue2 的方案,EventEmitter 和 MutationObserver 跟 Vue3 响应式无关。就像公示牌的智能感应膜,任何动作都逃不过它。
Q2. 与 Vue2 相比,Vue3 的 Proxy 方案能感知哪些对象操作?
- A.对象新增属性← 正确答案
- B.对象删除属性← 正确答案
- C.数组下标赋值← 正确答案
- D.函数调用本身
答案:ABC。Proxy 的 get/set/deleteProperty 等钩子让新增、删除、数组下标赋值统统可感知;D 是干扰项,函数调用本身与对象属性变更无关。如同感应膜能感知贴新价签、撕旧价签、改任意一格,但它不会“感知谁叫了老板一声”。
Q3. Vue3 中 track(依赖收集)发生在什么时机?
- A.读取响应式属性时← 正确答案
- B.写入响应式属性时
- C.组件卸载时
- D.浏览器刷新时
答案:A。响应式属性的 get 钩子会在读取时调用 track,把当前 effect 登记进小本本;写入时触发的是 trigger。正如同顾客看公示牌时老板才在本子上记他的名字,不是改价时才记。
Q4. trigger 派发更新时,effect 是从哪里取出来的?
- A.组件实例上
- B.该属性的依赖集合中← 正确答案
- C.全局事件总线里
- D.DOM 节点上
答案:B。每个属性都有自己的 Dep 小本本,trigger 按 key 找到对应 Dep,再遍历其中的 effect 集合。就像改白菜价只翻“白菜价”那本册子,不翻别的册子。
Q5. 解构 reactive 对象后想保持响应式,正确的做法是?
- A.使用 toRefs(state)← 正确答案
- B.用 JSON 深拷贝一份
- C.改用原生普通对象
- D.用 Object.freeze 冻结
答案:A。toRefs 把每个属性变成独立的 ref,解构出的变量仍连着感应膜;深拷贝和普通对象都会切断响应式联系。就好比你要把公示牌上的某一格带走,得带走一块“独立的小感应牌”而不是抄个数字。
Q6. 在 Composition API 中,setup 函数在什么时机执行?
- A.组件卸载之后
- B.组件创建后、首次渲染前← 正确答案
- C.每次点击事件时
- D.模板编译时
答案:B。setup 在组件实例创建后马上执行,此时 props 已可用,返回值供模板渲染使用。如同后厨开火前先把“买菜流程卡”准备好,菜端上桌(渲染)之前流程已经设计完毕。
Q7. Vue3 把同一次更新周期内多次触发通知合并成一次刷新,核心机制是什么?
- A.异步微任务队列去重← 正确答案
- B.同步递归调用
- C.定时器轮询
- D.Web Worker 多线程
答案:A。trigger 把 effect 丢进 Set 队列,同一 effect 重复入队被去重,等同步代码跑完再用微任务统一 flush。相当于老板先把所有要喊的人记在小黑板上,傍晚统一广播一次,而不是每改一次价就吼一嗓子。
Q8. 关于 computed 的特性,以下哪些描述是正确的?
- A.依赖不变时缓存结果← 正确答案
- B.惰性计算,读取时才求值← 正确答案
- C.依赖变化后自动重新计算← 正确答案
- D.每次读取都完整执行一遍计算
答案:ABC。computed 带缓存:依赖不变直接返回缓存,首次读取才求值,依赖变化后标记脏值并自动重算。D 恰恰是它要避免的行为——电子秤只在读数时称重,并缓存上一次的称重结果。
Q9. 在自定义组合函数中,多个组件复用同一段响应式逻辑时,各组件的数据状态是?
- A.完全共享的全局状态
- B.各自独立的响应式状态← 正确答案
- C.由浏览器随机分配
- D.只在第一个组件中生效
答案:B。组合函数调用时每次内部都会重新创建 ref/reactive,所以不同组件拿到的是各自独立的响应式状态;若要共享需借助外部容器或 Pinia。如同每个摊位都可以用同一张“买菜流程卡”,但各家买回来的菜放进各自菜篮。
Q10. 以下哪些是 Vue3 为提升响应式系统性能而采用的手段?
- A.effect 去重批量刷新← 正确答案
- B.computed 结果缓存← 正确答案
- C.精确到属性的依赖追踪← 正确答案
- D.全局禁用 Proxy 改用事件总线
答案:ABC。A、B、C 分别对应队列去重、惰性缓存、按 key 精准通知,都是 Vue3 性能优化核心手段。D 完全违背设计方向——事件总线既不精准也难追踪,Vue3 不可能放弃 Proxy 回到旧路。