想象你是一座城市的水电调度工程师。旧城区每栋楼用的都是老式门禁——想监控某个住户的用水量,你得提前在每户的管道口装一个“小监控器”(Object.defineProperty),装完才发现:新接入的管线它看不见、数组管道年久失修也锁不住。于是你和团队彻底翻新,给每栋楼装上“智能总闸”(Proxy):所有进出房屋的操作都从总闸过一遍,不用预埋监控器,新增管线、深层管道、数组分支统统尽收眼底。这座“城市水电系统”就是 Vue3 的响应式原理,而那套重新设计的“调度总控台”就是组合式 API。 本文会带你从零看明白 Vue3 如何用 Proxy 实现 ref/reactive 的响应式,理解 effect 调度与计算属性缓存的底层逻辑,再对比 Vue2 的 defineProperty 为什么被取代。你会亲手搭建一个迷你响应式系统,并搞懂高频面试里最常考的“依赖收集、派发更新、批量调度”到底是怎么回事。
对 AI 从业者而言,Vue 的响应式系统是“状态 → 副作用自动同步”的经典范本——无论做前端交互可视化、LLM 流式输出界面,还是 agent 状态机,理解“数据订阅-变更通知”模型都能帮你写出高性能、低心智负担的交互代码。
目录
可视化演示
展示Vue3 Proxy响应式系统、依赖收集(track/trigger)机制、ref与reactive对比及computed缓存与懒计算原理,含流动波形背景和发光动画,交互切换各机制视图。
本节配有交互动效(进入视口后自动加载,约 3KB)。
组合式API:把零散工具装进功能柜
从前写 Vue2 组件,像把扳手、钳子、螺丝刀全倒进一个大抽屉:data 放一起、computed 放一堆、watch 又堆一角。一个数据相关的逻辑被硬生生拆成三块,改需求时你得在抽屉里翻来翻去。组合式 API 给这个抽屉装上了“功能分隔板”——每个业务关注点(比如“用户列表”)都收进一个独立的 setup 函数块里,代码按逻辑而非选项类型组织。
setup 是组合式 API 的入口,组件创建时执行一次,返回的变量和方法直接暴露给模板。你可以把“拉取用户列表、搜索过滤、分页”这些代码写进同一个 setup 作用域,再用自定义组合式函数(如 useUserList)抽离复用。这不是什么魔法,本质上是 JavaScript 闭包与函数式组合的运用——就像把同一套水电管线按房间收纳进对应工具柜。
相比选项式 API 分散的数据、方法、侦听器,组合式 API 最大的好处是“高内聚、低耦合”。逻辑复用从 mixin 的“叠加冲突”变成函数间的“自由组合”,TypeScript 推导也更自然。
最终,组合式 API 让组件变成“一个入口、多套功能包”的结构——新增功能不需要翻动旧代码,就像在工具柜里加一个新抽屉。这种方式还被广泛用于封装可复用的业务 Hooks,是 Vue3 生态设计的第一块基石。
- setup 标识$\text{setup}_{组件}$ 表示 Vue3 组件执行的核心函数,相当于工具柜的“柜门把手”。
- 状态引用$\text{state}_{ref}$ 是组件内用 ref/reactive 声明的响应式状态,如同柜内收纳好的管线总阀。
- 副作用逻辑$\text{logic}_{effect}$ 是用 watchEffect/effect 注册的副作用,当依赖变化时自动运行,对应柜内的电工检修流程。
- 派生数据$\text{computed}_{缓存}$ 表示计算属性,只读且带缓存,是柜内“即取即用”的便捷工具。
响应式Proxy:从门禁卡到智能总闸
Vue2 用 Object.defineProperty 做响应式,如同给每栋楼的每一扇门都安装一台固定式刷卡机。装之前得知道“这栋楼有哪些门”——所以对象新增属性、删除属性,Vue2 都有 forceUpdate 也用不上的死角;数组更是老大难,改 arr[2] = x 和 arr.length-- 这种操作,刷卡机完全没记录。每装一台机器还耗内存,为了响应式你不得不给所有属性预先“装修”一遍。
Vue3 直接升级成了“楼宇智能总闸”——Proxy。它代理的是整栋楼而非每扇门,所有读写、删改、枚举操作都先经过总闸。这意味着:新增属性天然可拦截、删除属性可拦截、数组索引与 length 修改也可拦截。而且代理是“惰性”的:只有访问到嵌套对象时,才给那一层再套一个 Proxy 子代理,内存开销按需分配。
Proxy 可拦截的 trap 有 get、set、has、deleteProperty、ownKeys 等 13 种。Vue3 的 reactive 正是用这些 trap 实现了“读取时收集依赖、写入时触发更新、删除时通知清理”的完整闭环。
正因为总闸拦截的是“楼宇级别的所有操作”,Vue3 才能优雅解决 Vue2 中对象增删和数组变更检测的盲区。这不仅是一次 API 升级,更是响应式设计范式从“预埋监控”到“统一代理”的根本转变。
- 代理创建$\text{target} \xrightarrow{\text{new Proxy}}$ 表示将原始对象 target 包装成代理,如同为整栋楼接入总闸。
- get 拦载获取属性时执行 $\text{track}(target, key)$,作用是登记当前正在运行的 effect 为“该 key 的订阅者”,就像住户路过总闸签个到。
- set 拦截写入属性时执行 $\text{trigger}(target, key)$,遍历该 key 上的订阅者列表并重新执行,相当于放水通知所有登记住户。
- 删除拦截$\text{deleteProperty}$ 触发删除通知,这是 Vue2 无法做到的“退租提醒”。
Ref与Reactive:水表盒子与整楼监控
城市里有两种监控方式。ref 是给单个水表装个透明盒子——不管里面装的是数字还是对象,你都通过盒子的 .value 读取和修改;reactive 则是给整栋楼装总闸,直接操作楼里的每一处管线,不用经手任何盒子和 .value。
ref 的底层其实很妙:如果传进去的是基本类型,它用一个 { value: 原始值 } 的对象包住,再对这个对象做 reactive 代理;如果传进去的是对象,则直接调用 reactive 完成深层代理。所以 ref(0).value 触发的是 get/set trap,这和 reactive(obj) 的机制同源。
reactive 只能代理对象(含数组和 Map/Set 等),它拦截的是“对象本身的操作”。但直接解构 reactive 对象会丢失响应式——因为解构拿到的是原始值的拷贝,总闸只认楼不认你手里的复印件。解决办法是用 toRefs 把每个属性变成一个 ref 引用,解构后依然保持响应。
实践中推荐“ref 全家桶”——统一用 ref 声明数据,几乎不需要区分基本类型和对象,心智负担最小;reactive 在需要深层对象批量操作时更省代码。总闸和盒子的选择,本质是在“统一性”与“灵活性”之间找平衡。
- 基本类型包装$\text{ref}(\text{数字})$ 会用对象包装原始数字,让 Proxy 能拦截它的读写,如同把水表装进一个可开合的盒子。
- 对象走 reactive$\text{ref}(对象)$ 内部直接复用 reactive 完成深层代理,不再额外套壳,如同盒子内嵌套了整栋楼的总闸。
- reactive 直接代理$\text{reactive}(\text{obj})$ 返回 Proxy 代理对象,访问属性自动深层懒代理,类似总闸按需为每个房间装分闸。
- toRefs 解构$\text{toRefs}(\text{reactive对象})$ 把每个属性转为 ref,使解构后仍保留响应连接,确保复印件也被总闸追踪。
依赖收集原理:住户登记,track 存档
想象总闸每天要做的第一件事:当住户 A 打开水龙头,总闸记录“A 需要这条管道的水”。这里的“住户”就是 effect(副作用函数),“管道”就是某个对象的某个 key。Vue3 用全局的 targetMap 登记这些订阅关系,结构是 Map(对象) → Map(key) → Set(effect),像一座三层楼的档案柜。
track 函数在 get 拦截中调用:先确定当前是否处于某个 effect 的同步执行中(activeEffect),如果是,就把这个 effect 塞进对应 key 的 Set 集合里。同一个 effect 重复读取同一 key 时,Set 的天然去重保证了订阅只登记一次,避免派发时重复执行。
有意思的是最外层用了 WeakMap 而非 Map。WeakMap 的 key 是弱引用——当某个响应式对象不再被任何代码引用时,它连同整个依赖档案会被垃圾回收,不会造成内存泄漏。这正是“整栋楼拆掉后,档案柜里的户主信息也跟着注销”。
有了这套档案,系统在任何时候都知道“哪些 effect 依赖哪个 key”。这也解释了为什么组合式 API 中副作用函数要同步执行完——只有同步读取过的属性才会被登记,异步读取发生在 effect 退出后,无法被追踪。
- 外层 WeakMap$\text{targetMap}$ 是弱引用 Map,键为原始对象 target,值是其属性索引,确保对象销毁后档案自动释放。
- 中层属性索引每个对象对应一个 $\text{Map}(\text{key})$,键为属性名,值为该属性的订阅者集合,像大楼每一层的水电线路图。
- 内层订阅集合每个 key 对应一个 $\text{Set}(\text{effect})$,存放所有读取过该 key 的副作用函数,Set 去重确保一个 effect 只登记一次。
- track 登记get trap 中执行 $\text{track}(\text{target}, \text{key})$,把 activeEffect 塞进 depSet,如同住户在管线登记簿上签名。
派发更新机制:放水通知,trigger 执行
当总闸检测到某条管道的水流变化(set 被调用),就要按档案逐个通知曾在登记簿上签名的住户“水来了”——这就是 trigger 的职责:遍历 targetMap 中该 target 对应 key 的 depSet,把里面的 effect 全部重新执行一遍。
trigger 有四个精细的注意点。第一,如果新值与旧值相同(Object.is 判断),直接不触发,避免无谓的放水;第二,遍历订阅者时用 new Set(dep) 迭代副本,防止 effect 执行过程中又增删订阅导致死循环;第三,数组的 length 变化要特殊处理——不仅要通知索引订阅者,还要通知所有依赖 length 或迭代器的 effect;第四,如果读取的是迭代器方法(如 forEach、Symbol.iterator),还需要单独收集额外的迭代 key 依赖。
这套流程保证了“改一个值,所有依赖它的计算、渲染、watch 全部自动刷新”。Vue3 的 trigger 是同步遍历的,但每个 effect 本身可能是“可调度的”——它不会立刻执行,而是被交给调度器排队,从而支持批量更新。
放水通知不是简单广播,而是一次精心设计的“精准投递”:该不通知的不打扰,通知时又要覆盖数组这类特殊用户的全部依赖。理解 trigger 的细节,就掌握了 Vue3 性能优化的内功心法。
- 值变更判定$\text{Object.is}(\text{旧值}, \text{新值})$ 为真则提前退出,如同送水前先看水箱是不是真的见了底。
- 迭代副本遍历$\text{depSet}$ 遍历前拷贝为 new Set(dep),避免 effect 中读写属性造成的“边遍历边改档案”死循环,也是派发时最经典的防御性设计。
- 数组拦截扩展修改数组索引与 length 时,额外触发数组迭代器的依赖集合,让 v-for 等迭代视图同步更新。
- 同步遍历执行$\forall \text{effect}$ 逐个执行(或交给调度器),完成从数据变化到视图更新的因果闭环。
Effect调度器:高峰期的交通指挥
直接通知住户会有问题:如果一场代码里改了 10 个状态,每个状态都触发一次渲染,界面会闪 10 次。于是 Vue3 给 effect 增加了一个可选的 scheduler 选项——effect 不立刻执行,而是被交给调度器“排队等号”。
调度器的工作方式类似地铁站的限流围栏:同一个 effect 被多次触发时,只在当前批次中执行一次(通过全局队列 + Set 去重保证)。Vue3 的响应式系统在浏览器微任务队列中批量刷新这些待执行 effect,从而实现“同步改数据、异步批渲染”。这就是为什么你连续修改 10 个 ref 后,页面只更新一次。
渲染 effect 的调度更进一步:render 函数本身就是一个 effect,数据变化时它不会立刻重渲染,而是被放进任务队列,等待组件更新时机统一 flush。这也引出了 nextTick 的原理——Promise.then 回调必然排在当前批次的 effect 刷新之后。调度器还预留了 pre/post 等执行阶段,上层的 watch 回调、组件渲染、用户回调都可以按需进入不同时间片。
调度器把同步世界和异步渲染优雅地连接起来。它不改变“数据变了要通知”的本质,却决定了“什么时候通知、通知几次”——正因如此,Vue3 才能在高频交互场景下保持 60fps 的流畅体验。
- 入队去重$\text{queue.add}(e)$ 用 Set 保证同一 effect 只入队一次,类似地铁刷码后的排队闸口防止同一乘客重复占位。
- 微任务调度$\text{Promise.resolve()}$ 后统一 flush 队列,把同步多次修改合并为异步一次执行,避免同一帧内重复渲染。
- 渲染 effect 排队$\text{renderEffect}$ 也通过调度器排队,组件更新延迟到队列统一执行从而实现“批量 DOM 更新”,配合虚拟 DOM 的 diff 优化效率。
- nextTick 时序$\text{nextTick}(\text{cb})$ 在队列 flush 之后运行回调,确保回调一定能拿到最新 DOM,如同等到末班车出站后再上车。
计算属性缓存:蓄水池的智慧
看一个水电系统场景:几百户住户每天都要看同一块总水表的读数,如果每次有人问就重新跑一遍整个供水系统算一次,浪费巨大。计算属性就是这个“蓄水池”——它把计算结果存下来,只有上游水源(依赖的响应式数据)真的变化时才重新蓄水。
computed 的实现机制非常优雅:它内部创建一个惰性 effect(lazy),并维护一个 _dirty 脏标记。首次访问 computed.value 时执行计算方法并缓存结果,标记置为 false;此后只要依赖不变,每次读取都直接返回缓存值,不再重算。当依赖变化时,computed 内部 effect 的 scheduler 被触发——它只把 _dirty 置回 true,并不立刻重算,等下一次有人访问 .value 时才“按需重算”。
因此 computed 天然具备两个特性:惰性(不访问不算)与缓存(访问过不重算)。这正是它和普通方法调用的本质区别——普通方法每次执行都跑一遍,computed 只在依赖变化时重算一次。
在模板中频繁使用计算属性,或者数据派生链较长时,computed 的缓存机制能显著降低计算开销。它也展示了“用空间换时间、用脏标记换按需计算”的经典工程思想——这套思想在 React 的 useMemo、zustand 的 selector 中同样随处可见。
- 脏标记初始化$\text{dirty} : \text{True}$ 表示初次读取时蓄水池是空池,必须重算。
- 首次读重算访问 .value 且 dirty 为 True 时执行 $f()$,将结果存入 cache,并把 dirty 置为 False,如同完成首次蓄水并闭闸。
- 依赖变更通知上游依赖 set 时,computed 的 effect 调度器仅执行 $\text{dirty} = \text{True}$,不开闸放水,实现“只标脏、不重算”。
- 惰性重算下次读取时发现 dirty=True 再重算,保证“没人要水就不重复蓄水”,这就是计算的极致懒惰。
AI 应用场景
大型中后台的状态管理重构
阶段:前端应用开发与状态管理层
解决的问题:传统中后台项目依赖 Vuex/mobx 管理全局状态,状态分散、模块耦合,跨组件共享响应式数据时容易产生“改一处、崩多处”的问题。用组合式 API 的 ref/reactive + computed 可以把用户信息、权限、偏好设置等收敛到独立组合式函数中,天然响应式且可测试。
具体案例:某 AI 平台的控制台需要展示模型训练状态(进度、日志、指标)。团队用 useTrainStatus() 封装轮询接口与 WebSocket 更新逻辑,内部用 ref 存储 status、computed 计算进度百分比,各组件只需调用函数并读取返回值,数据自动同步。新增“训练指标图表”时,直接组合已有函数,无需改动状态层。
实时数据看板的高频更新优化
阶段:实时数据流接入与渲染优化
解决的问题:AI 推理结果的流式输出(如 LLM 打字机效果、指标监控大屏)每秒可能产生数十次数据更新,若每次更新都直接驱动 DOM,浏览器必然卡顿。Vue3 的 effect 调度器将同批次更新合并到微任务队列,配合 computed 缓存派生指标,可实现高性能流式渲染。
具体案例:一个 LLM 对话应用用 ref('') 保存流式输出的文本片段,每当收到新的 token 就追加到字符串上。由于多次 append 发生在同一个事件循环内,渲染 effect 被调度器合并,页面不会逐字闪烁,而是以帧为单位平滑刷新。对 token 进行长度、关键词高亮等派生计算时用 computed 避免重复分词。
通用交互逻辑的 Hooks 封装
阶段:跨项目组件复用与工程化
解决的问题:在多个 AI 产品中,“拖拽上传文件、轮询任务状态、倒计时等待”等交互逻辑高度相似,选项式 API 很难优雅抽取复用。组合式 API 的本质是函数组合,可以像 React Hooks 一样把可复用逻辑封装为 useXxx 组合式函数。
具体案例:团队封装了 useModelInference() 处理大模型调用的全生命周期:loading(ref)、结果(ref)、错误(ref)、重试(computed 派生剩余次数)。不同业务组件只需三行代码接入,也能与其他组合式函数自由嵌套,例如在流式输出中追加 useAutoScroll 实现对话自动滚动。
Python 演示
# -*- coding: utf-8 -*-"""mini_vue3.py极简版 Vue3 响应式系统:实现 ref / reactive / effect / computed 核心语义"""from copy import deepcopy class Dep: """依赖收集器:每个属性对应一个 Dep 实例,维护订阅它的 effect 集合""" def __init__(self): self.subs = set() def depend(self, effect): if effect is not None: self.subs.add(effect) def notify(self): for fn in list(self.subs): fn() class Reactive: """Proxy 风格响应式对象:get 收集依赖,set 触发更新""" def __init__(self, target, is_ref=False): self._target = target self._deps_map = {} self._is_ref = is_ref def _get_dep(self, key): if key not in self._deps_map: self._deps_map[key] = Dep() return self._deps_map[key] def get(self, key): dep = self._get_dep(key) dep.depend(_active_effect) value = self._target[key] if isinstance(value, dict) and not isinstance(value, Reactive): nested = Reactive(value) self._target[key] = nested return nested if isinstance(value, Reactive): return value return value def set(self, key, obj_value): old = self._target.get(key) if old == obj_value: return self._target[key] = obj_value dep = self._get_dep(key) dep.notify() _active_effect = None class Ref: """ref 的 Python 模拟:维护一个 _reactive 对象,value 属性走 get/set""" def __init__(self, raw_value): self._reactive = Reactive({"value": raw_value}, is_ref=True) @property def value(self): return self._reactive.get("value") @value.setter def value(self, new_val): self._reactive.set("value", new_val) def reactive(obj): return Reactive(obj) def ref(obj_value): return Ref(obj_value) def _run_effect(fn): global _active_effect _active_effect = fn try: fn() finally: _active_effect = None def effect(fn): _run_effect(fn) return fn def computed(fn): _cache = {"value": None, "dirty": True} def runner(): _cache["value"] = fn() _cache["dirty"] = False wrapped = lambda: None effect(lambda: (_cache.__setitem__("dirty", True), wrapped())) return { "get_value": lambda: (runner() if _cache["dirty"] else None, _cache["value"])[1], } if __name__ == "__main__": count = ref(0) double = computed(lambda: count.value * 2) log = [] def watch_count(): log.append(count.value) effect(watch_count) count.value = 5 print("double cached:", double["get_value"]()) count.value = 10 print("double after change:", double["get_value"]()) print("watch log:", log)
class Dep类比依赖集合 Set(effect):每个属性一个 Dep,depend 登记订阅者,notify 派发更新。_active_effect全局标记“当前正在执行的 effect”,这就是 Vue3 中 activeEffect 的实现方式。def get(self, key)读取属性时调用 dep.depend(_active_effect),完成依赖收集,相当于 track。def set(self, key, obj_value)写入属性时对比旧值并 dep.notify(),相当于 trigger。count = ref(0)ref 内部把值包进{"value": ...},再交给 Reactive 代理,和 Vue3 底层如出一辙。effect(watch_count)同步执行一次用于收集依赖,之后数据变化自动重跑。count.value = 5setter 触发通知,watch_count 被重新执行,log 中追加 5。double["get_value"]()computed 首次读取时重算并缓存,依赖不变时后续读取直接命中缓存。_cache["dirty"]脏标记:依赖变化时仅置脏不重算,下次读取才惰性重算。effect(lambda: ...)computed 内部通过一个 effect 监听依赖,当依赖变化时把 dirty 置为 True。
常见误区
误区“ref 是给数字、字符串用的,普通对象必须用 reactive,否则会报错或丢失响应式。”
正解ref 内部其实不分类型:传基本类型就包装成 { value: 原值 },传对象就直接调用 reactive 做深层代理。所以 ref({ count: 0 }) 完全可以正常工作,且 state.value.count = 1 也是响应式的。唯一的差别是访问方式——ref 需要 .value。工程上“全部用 ref”是完全可行的统一方案,不会丢响应式。
为何常见:初学时看教程总说“ref 基本类型、reactive 对象类型”,很容易把“使用惯例”误解为“底层限制”。
误区“const { name } = reactive({ name: '张三' }) 之后 name 还是响应式的,改它会更新视图。”
正解解构拿到的是原始值的拷贝或原始引用,只有通过对象本身访问才能触发 Proxy 的 get/set trap。解构后的 name 已经脱离了代理对象,所有读写都绕过总闸,自然丢失响应式。需要用 toRefs(state) 把每个属性转成 ref 后再解构,才能保持连接。
为何常见:JS 解构太常用,直觉上会觉得“从响应式对象里拿出来的东西也是响应式的”,忽略了 Proxy 拦截依赖“引用关系”这一事实。
误区“effect 注册后不会立即执行,只有数据变化时才触发。”
正解恰好相反,effect(fn) 注册后会立即同步执行一次,目的就是“跑一遍函数体,触发所有属性读取,从而完成依赖收集”。之后数据变化时才会再执行。Vue3 中 watchEffect、computed 内部都有这个首次执行收集依赖的阶段。如果首执行不跑,系统根本不知道这个 effect 依赖哪些数据。
为何常见:从“监听”的字面意思出发,容易类比 setTimeout 的“延迟回调”,但副作用函数必须“先跑一次暴露依赖”,这正是响应式系统设计的关键。
误区“Vue2 里数组 push 需要特殊处理,Vue3 虽然用 Proxy,但 push 这种原型方法应该也拦不住吧?”
正解Vue3 的 Proxy 不仅能拦截 arr[2] = x 这样的索引写入,还能拦截 push、pop、shift、splice 这类调用——因为 get trap 会先拦截到 arr.push 的读取,返回一个被包裹的方法,方法内部再经过 set 和 length 修改的 trap,最终都能触发依赖更新。数组在 Vue3 中完全不需要 hack。
为何常见:被 Vue2 的数组缺陷“创伤”后,容易惯性认为数组原生方法永远是个问题,没意识到 Proxy 对“原型链方法调用”同样有拦截能力。
误区“computed 就是带缓存的 getter,所以每次读取它都会先检查依赖有没有变,变了就重算,没变就返回缓存。”
正解这种理解方向是对的,但漏掉了“惰性”这个关键细节。computed 在依赖变化时不会立即重算,只是把脏标记置为 true;只有下一次访问 .value 时才会触发重算并更新缓存。也就是说计算被推迟到真正“被需要”的瞬间,这才是 computed 高效的本质。
为何常见:接触“缓存失效”机制时,容易默认“失效 = 立即重建”,而 Vue3 选择的是“失效 = 标记待重建”,把重算推迟到读取时,省掉了大量无用户访问的无效计算。
面试题
Q1. Vue3 中创建基本类型响应式引用应该用哪个 API?
- A.ref← 正确答案
- B.reactive
- C.computed
- D.watch
答案:A。ref 专为包装基本类型设计(内部也支持对象),返回带 .value 的响应式引用;reactive 只能代理对象;computed 是派生数据;watch 是侦听器,都不负责创建基本类型响应式。
Q2. Vue3 的响应式底层基于哪个 JS 特性实现?
- A.Proxy← 正确答案
- B.defineProperty
- C.Object.observe
- D.MutationObserver
答案:A。Vue3 用 Proxy 代理整个对象,无需预声明属性即可拦截增删改;defineProperty 是 Vue2 的方案;Object.observe 已被废弃;MutationObserver 用于监听 DOM 变化而非数据。
Q3. 关于 Vue3 的 Proxy 与 Vue2 的 defineProperty 对比,正确的是哪些?
- A.Proxy 可监听新增属性← 正确答案
- B.Proxy 可监听删除属性← 正确答案
- C.defineProperty 可监听数组索引
- D.Proxy 支持深层惰性代理← 正确答案
答案:ABD。Proxy 的 deleteProperty、set trap 天然覆盖新增和删除属性;defineProperty 对数组索引和 length 的监听存在盲区——严格说 defineProperty 无法拦截 arr[3]=x 这类新增索引赋值,因此 C 错误;Proxy 在访问嵌套对象时才递归代理,是惰性深层代理。
Q4. 从 reactive 对象解构出的属性为什么会丢失响应性?
- A.解构是值拷贝操作,绕过 Proxy← 正确答案
- B.reactive 不支持解构语法
- C.解构会销毁原代理对象
- D.解构返回的是 ref 引用
答案:A。解构提取的是属性当前值或原始引用,之后对解构变量的读写不再经过 Proxy 的 get/set trap,响应连接断裂;reactive 当然支持解构语法(JS 层面合法);解构不会销毁原对象;解构返回原始值而非 ref。用 toRefs 可以修复。
Q5. Vue3 派发更新(trigger)时为什么用迭代副本遍历依赖?
- A.避免 effect 执行中修改依赖导致死循环← 正确答案
- B.提升遍历性能
- C.确保依赖按顺序执行
- D.减少内存占用
答案:A。effect 执行时可能再次读取或写入同一属性,导致订阅集合在遍历过程中被增删;先拷贝一份 new Set(dep) 再遍历,保证原始集合变化不影响当轮迭代,避免死循环或漏执行。与性能、顺序、内存无直接关系。
Q6. 关于 effect 调度器(scheduler)的作用,正确的是哪些?
- A.批量合并同一 effect 的多次触发← 正确答案
- B.将 effect 放到微任务队列统一执行← 正确答案
- C.让所有 effect 变成异步的
- D.实现 nextTick 等待 DOM 更新← 正确答案
答案:ABD。调度器用 Set 去重,把同一 effect 多次触发合并为一次;渲染 effect 等通过 Promise 微任务统一 flush,因此 nextTick 可以保证拿到更新后的 DOM;但并非所有 effect 都是异步的——没有配置 scheduler 的 effect 仍同步执行,C 错误。
Q7. computed 的脏标记(dirty)在什么情况下会被置为 true?
- A.依赖的响应式数据发生变化时← 正确答案
- B.每次访问 computed.value 时
- C.computed 内部 effect 首次执行时
- D.组件销毁时自动置脏
答案:A。computed 内部 effect 的 scheduler 在依赖变化时只执行 dirty = true,不做重算;下次访问 .value 才惰性重算。首次执行 effect 是收集依赖,与置脏无关;同样组件销毁也不会置脏。
Q8. 修改数组的 length 属性时,Vue3 的 trigger 会额外通知哪些依赖?
- A.依赖数组迭代器的 effect← 正确答案
- B.依赖数组首个元素的所有 effect
- C.仅依赖 length 本身的 effect
- D.所有对象类型的依赖集合
答案:A。length 变化影响数组遍历结果,因此依赖 forEach/map 或 Symbol.iterator 的 effect 必须重新执行;只通知 length 自身的订阅者会漏掉 v-for 等迭代渲染。B、D 范围不准确。
Q9. 关于 ref 的底层实现,下列说法正确的是哪些?
- A.ref 传基本类型时会包装成 { value } 对象← 正确答案
- B.ref 传对象时内部调用 reactive 处理← 正确答案
- C.ref 与 reactive 的响应式机制完全无关
- D.ref 的 .value 读写同样经过 Proxy← 正确答案
答案:ABD。ref 统一把值包进 { value: ... } 再交给 reactive,因此基本类型被包装、对象直接走深层代理,.value 读写经过 Proxy 的 get/set trap;所以说 ref 与 reactive 机制同源,C 错误。
Q10. 嵌套对象在 Vue3 的 reactive 中何时被代理为深层 Proxy?
- A.对象创建时立即递归代理
- B.首次访问该嵌套对象属性时← 正确答案
- C.首次 render 完成后统一代理
- D.手动调用 toRaw 时才会代理
答案:B。Vue3 采用惰性深层代理:只有 get trap 访问到嵌套对象时才用 reactive 递归包装并缓存,避免创建时对所有深层属性做无谓代理,节省初始化的内存和时间开销。这也使 Vue3 在深层大对象上远比 Vue2 高效。