响应式与代理模式

共 17 题
📑 题目列表 17 题
#
★★★

1. Proxy 与 Object.defineProperty 在 Vue 2/3 响应式系统的对比

Vue 2 与 Vue 3 分别使用 Object.defineProperty 与 Proxy 实现响应式,两者有何差异与取舍?

  • Object.defineProperty 只能拦截已有属性(Vue 2)
  • Proxy 可拦截任意属性增删、数组、Map/Set(Vue 3)
  • 性能与兼容性取舍

Vue 2 用 Object.defineProperty 对对象属性做 getter/setter 改写,实现属性级拦截;其局限是只能拦截"已存在"的属性,新增/删除属性无法自动响应(需用 Vue.set/delete),数组的索引与 length 变更也难以拦截(需重写数组方法)。Vue 3 改用 Proxy 对整个对象做代理,可拦截 get、set、has、deleteProperty、ownKeys 等全部操作,因此能监听属性的新增、删除以及数组、Map/Set 等内置集合的变更,语义更完整。性能上 Proxy 无需逐属性递归改写,且采用惰性代理(访问时才递归),初始化和深层访问更高效;但 Proxy 是 ES6 能力,Vue 3 放弃了 IE 兼容。取舍总结:Vue 3 的 Proxy 表达力更强、性能更好、代码更简洁,代价是兼容性要求更高;Vue 2 的 defineProperty 兼容旧浏览器但需 Vue.set/delete 处理边界。工程上迁移到 Vue 3 后,新增/删除属性与数组操作无需再依赖特殊 API。

本题对比两种响应式实现的核心差异。回答要点出"defineProperty 只能拦截已有属性、需手动处理新增/删除与数组"与"Proxy 拦截全部操作、天然支持集合与增删"这一根本区别,并说明性能与兼容性的取舍。

#
★★★

2. Proxy 模式在 Vue 3 响应式系统的实现

Vue 3 响应式系统如何用 Proxy 实现?其 get/set 拦截与依赖收集的流程是怎样的?

  • reactive 用 Proxy 包装对象
  • get 拦截触发依赖收集(track)
  • set 拦截触发更新(trigger)

Vue 3 的 reactive 通过 new Proxy 把对象包装为代理,重写 get/set/has/deleteProperty 等陷阱。当读取属性时,get 拦截会调用 track 进行依赖收集——把当前正在执行的 effect(副作用)与该属性记录到依赖表(target 的 key → 依赖集合);当写入属性时,set 拦截调用 trigger 触发该属性相关的所有 effect 重新执行。嵌套对象是惰性代理的:get 时若值是对象,才递归代理(reactive),避免初始化时深度遍历,提升性能。此外还处理了避免重复代理(已代理对象不再代理)、区分原始对象与代理(toRaw)等细节。相比 defineProperty 的逐属性改写,Proxy 对整对象代理、拦截操作更全面,代码更简洁,且天然支持数组与集合。这就是 Vue 3 响应式"读取时收集依赖、写入时发布更新"的核心机制。

本题考察 Vue 3 响应式的实现细节。回答要说明 get 拦截收集依赖(track)、set 拦截触发更新(trigger)的闭环,以及惰性递归代理嵌套对象、避免重复代理等关键点,体现对响应式原理的深入理解。

#
★★★

3. 依赖收集(track/trigger)的双向链表与栈式依赖在大型应用的性能差异

Vue 3 依赖收集的"双向链表 + 栈式依赖"实现比 Vue 2 的数组/Set 方案在大型应用中有何性能差异?

  • Vue 2 依赖收集用数组 + Dep 类
  • Vue 3 用双向链表组织依赖(effect 链)
  • 栈式依赖(activeEffect 栈)处理嵌套 effect

Vue 2 中每个响应式属性有一个 Dep,依赖以数组或 Set 存储,effect 与 dep 间的关联用迭代逐项查找/清理,嵌套 effect 时用队列管理,存在清理开销与重复触发问题。Vue 3 改用了双向链表组织依赖:每个依赖(effect)既是 dep 的节点,也通过 added/next 链接,可以 O(1) 插入、删除与遍历,避免 Vue 2 中数组 splice 与 indexOf 的 O(n) 操作;同时用"栈式依赖"(activeEffect 栈)记录当前正在执行的 effect 链,支持嵌套 effect(如 computed 内访问响应式)的正确收集与恢复。在大型应用(大量组件与依赖)中,这种结构让依赖的注册、清理、批量触发更高效,减少了无效的重复执行与 GC 压力,配合组件级渲染调度(更新队列批量 flush)显著提升性能。性能差异主要体现在"高频依赖变更场景下的清理与重收集成本"以及"嵌套 effect 的准确性"。

本题考察 Vue 3 响应式内部的性能优化。回答要对比 Vue 2 数组/Set 的 O(n) 清理与 Vue 3 双向链表 O(1) 操作,并说明栈式 activeEffect 处理嵌套 effect 的机制,点出在大型应用中对依赖清理与触发效率的实际收益。

#
★★★

4. Vue 的 customRef 与 triggerRef 在异步响应的工程价值

Vue 的 customRef 与 triggerRef 如何支撑异步响应与手动触发更新?其工程价值是什么?

  • customRef 自定义依赖收集与触发
  • triggerRef 手动触发 ref 更新
  • 异步响应(延时、防抖、外部数据源)

customRef 允许开发者自定义一个 ref 的"依赖收集(track)"与"触发更新(trigger)"逻辑,从而在读写之间注入自定义行为,典型场景是实现防抖/节流的响应式输入:在 set 时延迟触发更新,避免每次输入都触发重渲染。triggerRef 则用于手动触发一个 ref 的更新,即使其 .value 没有被响应式替换也能强制通知依赖方重新执行,常用于绕过响应式代理直接修改深层对象后手动刷新。工程价值:customRef 让"异步响应"(延时、防抖、请求防抖、外部数据源同步)与响应式系统结合,无需手动管理 update 时机;triggerRef 在"读取了值但需要强制刷新"的场景(如 canvas 内部直接改对象、第三方库直改响应式数据)提供兜底。两者共同点是把"何时触发更新"的控制权交给开发者,提升异步场景的灵活性与可维护性。

本题考察 customRef 与 triggerRef 的应用价值。回答要说明 customRef 自定义 track/trigger 实现防抖等异步响应,triggerRef 绕过代理手动触发更新,并点出它们在异步与强刷新场景下的灵活性。

#
★★

5. 迭代器模式在前端数据流(迭代器协议)的应用

迭代器模式在前端数据流(迭代器协议)中有哪些应用?如何利用迭代器统一处理数据?

  • 迭代器协议(Symbol.iterator 与 next)
  • 数据流的统一遍历与惰性处理
  • Generator、异步迭代(for await...of)

迭代器模式让不同数据结构提供统一的遍历接口,前端数据流大量依赖迭代器协议:数组、Map、Set、字符串等内置类型都实现 Symbol.iterator,可用 for...of 统一遍历;自定义数据源(分页数据、流式数据、事件流)实现迭代器后可被统一消费。Generator 通过 yield 生成惰性序列,适合处理无限或流式数据(如分页加载、Socket 消息流),按需取值避免一次性载入内存。异步迭代器(for await...of)配合 async generator 处理异步数据流(如多条网络请求、流式响应),统一了异步序列的消费方式。工程价值:抽象数据遍历、惰性求值降低内存、把"数据如何产生"与"数据如何消费"解耦,提升代码复用与可读性。前端框架与库(如 RxJS 的 Observable 协议、Node 的流)也借鉴了迭代式数据流思想。

本题考察迭代器协议在数据流中的应用。回答要说明统一遍历接口、Generator 惰性序列、异步迭代处理异步数据流,并强调"生产与消费解耦",体现对前端数据流处理的理解。

#
★★

6. Proxy 实现响应式,get/set 拦截与依赖收集?

如何用 Proxy 实现简单的响应式系统?get/set 拦截与依赖收集如何配合?

  • Proxy 的 get/set 陷阱
  • get 收集依赖(track)
  • set 触发更新(trigger)

用 Proxy 实现响应式,核心是"读取时收集依赖、写入时触发更新"。需要一个全局的"当前 effect"栈,以及一个依赖表(Map:target → Map:key → Set of effects)。使用 Proxy 包装对象:get 陷阱中调用 track(target, key),把当前 effect 加入该 key 的依赖集合;set 陷阱中调用 trigger(target, key),取出该 key 的依赖并依次执行。外层用一个 effect 函数包裹用户代码,执行时把自身压入栈,结束弹出,从而让读取操作能正确收集到当前 effect。这样"谁读了这个属性,谁就在该属性变化时被重新执行"。实现要点:用 WeakMap 存 target 的依赖避免内存泄漏、避免重复代理(WeakMap 缓存已代理对象)、get 时对嵌套对象递归代理。这套机制就是 Vue 3 响应式的最小实现,也是手写响应式面试题的核心。

本题考察手写响应式的能力。回答要给出 track/trigger 的依赖表结构与 get/set 拦截的配合,说明 effect 栈如何让读取收集到正确依赖,并点出 WeakMap、防重复代理等实现细节,体现对响应式原理的掌握。

#
★★

7. toRaw 与 markRaw 在绕过响应式代理(第三方库对象、性能敏感对象、跨库引用)时的工程价值与逃逸风险?

toRaw 与 markRaw 如何用于绕过响应式代理?它们有何工程价值与逃逸风险?

  • toRaw 获取原始对象(绕过代理)
  • markRaw 标记对象永不响应
  • 逃逸风险(绕过后失去响应,或破坏响应式一致性)

toRaw 返回 reactive 代理背后的原始对象,用于在需要原始引用时(如传给不识别代理的第三方库、序列化、比较原始身份)绕过代理;markRaw 把一个对象标记为"永不转为响应式",用于性能敏感或需保持原样引用的对象(如大型画布数据、第三方库实例、DOM 引用)。工程价值:避免代理开销与干扰、让第三方库能正确处理对象、保持跨库引用的一致性。逃逸风险:用 toRaw 拿到原始对象后直接修改,不会触发响应式更新,导致视图与数据不一致;markRaw 的对象即使被 reactive 包裹也保持原始,若其内部有应响应式追踪的数据则失去响应。此外把原始对象与代理对象混用(只修改其一)会造成状态分叉。因此应仅在明确需要"绕过代理"的场景使用,并注意绕过后主动管理更新(如结合 triggerRef 手动刷新)。

本题考察 toRaw/markRaw 的用途与风险。回答要说明两者的适用场景(第三方库、性能敏感、跨库引用),并重点指出"绕过代理后失去响应、状态分叉"的逃逸风险,强调需谨慎使用并配合手动更新。

#
★★

8. shallowRef 与 shallowReactive 在大型不可变数据(列表、画布对象)上的性能取舍,以及 triggerRef 手动触发更新的时机?

shallowRef 与 shallowReactive 如何优化大型不可变数据(列表、画布对象)的性能?triggerRef 手动触发更新的时机是什么?

  • shallowRef 只代理 .value 不深代理
  • shallowReactive 只代理顶层属性
  • 不可变数据结构(整体替换)场景

shallowRef 只对 .value 做响应式,不对其内部对象深代理;shallowReactive 只对对象顶层属性做响应式。当数据结构"整体替换"(如不可变更新:传入新数组/新对象)而非原地修改时,浅层响应式足以感知变化,且避免了深层代理的遍历与代理开销,适合大型列表、画布状态、图表数据等场景——每次更新都替换整个对象,深层代理无意义反而拖慢性能。triggerRef 用于在"值已变化但响应式系统未感知"时手动触发更新,例如:用 shallowRef 持有对象,原地修改了其某属性后调用 triggerRef(ref) 强制依赖方重新执行;或绕过代理直接改深层数据后手动刷新。时机判断:需要"手动刷新"的典型场景是"浅层代理 + 原地修改"或"使用 toRaw 绕过代理后";若总是整体替换,则无需 triggerRef。取舍:浅层响应式牺牲了"深层自动收集"的便利,换取性能与可预测性,适合对内部结构不关心、以整体替换为主的场景。

本题考察浅层响应式与手动触发。回答要说明 shallowRef/shallowReactive 只做浅层代理,结合"整体替换式不可变更新"说明其性能价值,并给出 triggerRef 的触发时机(浅层代理原地修改、绕过代理后)。

#
★★

9. Reflect API 在代理拦截的元数据获取与返回值控制

Reflect API 在 Proxy 拦截中如何用于元数据获取与返回值控制?为什么通常用 Reflect 而非直接操作?

  • Reflect.get/set 等与目标对象默认行为一致
  • 保证语义正确(this 绑定、返回值、可写性)
  • 元数据获取与返回值控制

Reflect API 提供了与对象内部方法对应的同名函数(Reflect.get/set/has/deleteProperty/ownKeys 等),在 Proxy 陷阱中调用它们能"转发到目标对象的默认行为",从而保证语义正确。例如在 get 陷阱中 return Reflect.get(target, key, receiver),用 receiver 作为 this,能正确处理 getter 中的 this 绑定;在 set 陷阱中 Reflect.set 的返回值反映是否有 strict 模式下的赋值失败,配合严格模式避免"明明赋值失败却返回 true"的隐式错误。未用 Reflect 时,直接 target[key] 会丢失 receiver 绑定、且 getter/setter 的 this 指向错误。元数据获取方面,Reflect 配合 getOwnPropertyDescriptor、ownKeys 可获取属性描述符与键集合,与 Proxy 的 ownKeys/getOwnPropertyDescriptor 陷阱配合实现"透明的元数据拦截"。工程上,用 Reflect 转发是 Proxy 陷阱的标准写法,能保证拦截行为与原生行为一致、结果可预期。

本题考察 Reflect 与 Proxy 的配合。回答要说明 Reflect 转发默认行为、正确处理 receiver/this 绑定与返回值,并指出不用 Reflect 可能导致的语义错误,体现对代理陷阱细节的理解。

#
★★

10. Proxy 在 MobX 与 Solid Signals 的实现差异

MobX 与 Solid 的 Signals 分别如何用 Proxy 实现响应式?两者有何差异?

  • MobX 用 Proxy 做可观察对象(observable)
  • Solid 用 Signals(细粒度响应)
  • 依赖收集方式与更新粒度差异

MobX 用 Proxy 包装对象实现可观察(observable),在 get 时收集依赖、set 时触发,采用"自动追踪 + 派生值"模型,依赖以树/图形式组织,更新通常是"运行中被激活的派生值"精确重算;它更强调"可观察对象 + 自动计算派生值",适合类/对象模型。Solid 采用 Signals(createSignal 的 getter/setter),本质上是用"存取函数"而非 Proxy 做细粒度响应,每个 signal 是独立的依赖节点,更新精确到读取该 signal 的组件/effect,不依赖组件树重新渲染,性能极高。差异在于:MobX 通过 Proxy 拦截对象属性的读写,可直接观察嵌套对象;Solid 的 signal 是显式声明的、通过 getter 调用收集依赖,不隐式拦截对象。两者都做"读取即收集、写入即更新"的细粒度依赖,但 MobX 代理对象、Solid 代理访问函数。取舍:MobX 更贴近"对象污染少、自动可观察",Solid 更显式、更新粒度更细、更利于编译期优化。

本题考察不同响应式库的实现差异。回答要说明 MobX 用 Proxy 观察对象、Solid 用 signal 存取函数做细粒度响应,并比较依赖收集方式与更新粒度,体现对响应式多样性的理解。

#
★★

11. 发布订阅与 EventEmitter 在前端事件总线的应用

发布订阅模式与 EventEmitter 如何在前端事件总线中应用?如何避免常见问题?

  • 发布订阅的 on/emit/off 模型
  • EventEmitter 实现(Node 的 events 模块思想)
  • 事件清理、命名规范、类型安全

发布订阅模式通过事件总线解耦发布者与订阅者,前端事件总线(EventEmitter)实现 on 监听、emit 触发、off 移除,用于跨组件、跨模块的松散通信。实现要点:bus 对象维护事件名 → 回调列表的映射,emit 时取出并调用;支持 once(只触发一次)、支持返回取消订阅函数。工程上需注意:其一,组件卸载时必须 off 移除监听,否则造成内存泄漏与重复触发;其二,事件名应规范统一(如常量枚举),避免字符串散落与拼写错误;其三,可结合类型系统(TS 泛型事件表)提升类型安全;其四,避免过度使用事件总线让数据流难以追踪,复杂场景优先用状态管理。EventEmitter 是 Node events 模块的思想,前端常用 mitt、EventEmitter3 或手写轻量实现。价值在于解耦、跨模块通信、事件驱动架构,但需配以严格的清理与命名纪律。

本题考察事件总线.回答要说明 on/emit/off 模型与实现要点,重点强调"卸载时取消监听防泄漏"、"事件命名规范"与"类型安全"、以及"避免事件总线滥用"的工程纪律。

#
★★

12. Vue 的 reactive 解包与 ref 自动解包在模板的边界

Vue 的 ref 自动解包与 reactive 解包在模板、响应式对象中如何工作?边界是什么?

  • ref 在模板与 setup 顶层自动解包
  • ref 作为 reactive 属性自动解包
  • 数组与嵌套对象中的解包边界

Vue 的 ref 在模板中访问时会自动解包(直接用 count 而非 count.value),在 setup 返回的顶层 ref 也会自动解包;当 ref 作为 reactive 对象的属性时会被自动解包,访问 reactiveObj.attr 直接得到值。但存在边界:其一,ref 作为数组元素或在 Map/Set 等集合中不会自动解包,需要 .value;其二,解包只发生在访问时,ref 本身仍是带 .value 的引用对象;其三,通过 toRefs 解构 reactive 得到的 ref 在模板中仍会解包,reactive 的响应式追踪不会因解包而断开。理解边界很重要:避免在数组/集合中误用 .value 缺省导致的 bug,以及理解"解包即自动取 .value"只是模板/响应式访问的便利,不改变对象本质。工程上,写模板时注意数组元素、深层嵌套中的 ref 需显式 .value。

本题考察 ref/reactive 解包的边界。回答要说明顶层与 reactive 属性自动解包、数组/集合不解包、解包不改变引用等边界,帮助识别实际编码中的 .value 使用误区。

#
★★

13. 代理模式在 Service Worker 的 Fetch 拦截应用

代理模式如何体现在 Service Worker 对 Fetch 的拦截中?其工程应用是什么?

  • Service Worker 拦截 fetch 请求
  • 缓存策略、离线支持、请求改写
  • 网络代理的"中间层"思想

Service Worker 是浏览器与网络之间的"代理层",可拦截页面发出的所有 fetch 请求,是代理模式的典型体现。通过监听 fetch 事件,可以决定请求如何处理:直接走网络、从缓存取、改写请求(如改 URL、加请求头)、组装响应(如离线 fallback 页面)、缓存优先或网络优先等策略。工程应用:离线/缓存优先(PWA 离线可用)、缓存静态资源提升加载、请求改写与重定向、请求失败兜底、网络监控与调试。这正体现了代理模式"在调用方与真实目标之间插入中间层,透明地拦截并增强请求"的思想。实现要点:需在 install 事件缓存资源、activate 清理旧缓存、fetch 事件中按策略响应,并处理跨域与 CORS 限制。价值在于让前端具备"网络层面的可编程代理能力",服务于离线、性能与可靠性。

本题考察代理模式在 Service Worker 中的体现。回答要说明 fetch 拦截实现"中间层代理",并列举缓存策略、离线支持、请求改写等应用,突出代理模式"透明拦截与增强"的本质。

#

14. Immer produce 与 mutable state 在 reducer 的工程价值

Immer 的 produce 如何在 reducer 中实现不可变更新?与直接 mutable state 相比有何工程价值?

  • Immer 用 draft 代理实现"可变更的不可变"
  • produce 简化 reducer 中的不可变更新
  • 结构共享与性能

Immer 的 produce 通过 Proxy 包裹 draft,允许开发者按"可变"方式直接修改 draft,produce 结束时自动生成新的不可变状态(未修改的部分通过结构共享复用原对象),从而在 reducer 里用直观的赋值语法实现不可变更新,避免手写展开运算符深层嵌套的繁琐与易错。工程价值:代码可读性高、减少深拷贝的样板与性能开销(结构共享让未变部分复用)、避免意外修改原 state 导致的不可控更新、便于 trace 与调试。相比手写 { ...state, a: { ...state.a, b: x } },Immer 让深层更新更简洁。注意事项:produce 内部必须用 draft 而非原 state,且 draft 是"暂时"的,派生数据需在返回后再处理;Immer 在热路径有少量代理开销,但结构共享通常抵消了深拷贝成本。在 Redux/Zustand 等 reducer 中是主流实践,把"不可变约束"与"可变书写"结合。

本题考察 Immer 在不可变更新中的价值。回答要说明 produce 的 draft 代理如何实现"可变更的不可变"、结构共享带来的性能与可读性收益,并点出使用注意(用 draft、draft 临时性),体现对不可变状态管理的理解。

#

15. Proxy 陷阱中 set 返回值与不变性约束(configurable/writable)

Proxy 的 set 陷阱返回值与不变性约束(configurable/writable)有什么关系?如何正确实现?

  • set 陷阱的返回值表示赋值是否成功
  • 严格模式下的语义
  • 不变性约束(可配置/可写属性)

Proxy 的 set 陷阱返回值表示"赋值是否成功":返回 true 表示成功,返回 false 表示失败。在严格模式下,若 set 返回 false(或抛出),赋值会抛出 TypeError;在非严格模式下则静默失败。因此必须返回与目标属性实际赋值结果一致的布尔值,通常用 Reflect.set(target, key, value, receiver) 的返回值作为返回,以保证语义正确。不变性约束(invariants):如果目标属性不可写(writable: false)或不可配置(configurable: false),则通过代理的 set 也不能成功改变其值,否则引擎会抛 TypeError,这是 Proxy 必须遵守的不变量,防止代理绕过目标对象的不可变性。正确实现要点:set 里先做自定义逻辑(如校验),再用 Reflect.set 转发并返回其结果,确保返回值与默认行为一致、且不违反不变性约束。忽略返回值或不遵守不变量是 Proxy 实现中的常见错误。

本题考察 Proxy set 陷阱的语义细节。回答要说明 set 返回值表示赋值成败、严格模式下的行为、Reflect.set 转发保证一致性,以及不可写/不可配置属性的不变性约束,体现对代理陷阱规范的理解。

#

16. Reflect 与 Proxy 的配合,为什么用 Reflect?

为什么在 Proxy 陷阱中要用 Reflect?Reflect 与 Proxy 如何配合?

  • Reflect 转发默认行为
  • receiver 的正确 this 绑定
  • 保持返回值语义一致

在 Proxy 陷阱中使用 Reflect 是为了"转发到目标对象的默认内部行为",并正确传递 receiver 等参数,从而保证拦截行为与原生行为一致。具体原因:其一,Reflect.get(target, key, receiver) 会把 receiver 作为 this 绑定传给 getter,若直接 target[key] 则 getter 的 this 指向 target 而非 receiver,导致自定义 getter 在代理下行为错误;其二,Reflect.set 的返回值反映赋值是否成功,能保证严格模式语义正确;其三,Reflect 的 deleteProperty、ownKeys、has 等也有对应内部方法,让陷阱能完整转发而不会遗漏底层语义;其四,配合不变性约束,Reflect 转发会遵守目标属性的可配置/可写规则,避免代理"越权"。因此 Reflect 是 Proxy 陷阱的"正确的转发器",开发者通常只写自定义逻辑,其余交给 Reflect 转发。

本题考察"为什么用 Reflect"。回答要聚焦 receiver 绑定、返回值语义、默认行为转发与不变性约束四点,说明 Reflect 让代理拦截行为与原生行为一致,是正确实现代理的关键。

#

17. 响应式系统的边界,数组、Map/Set 的特殊处理?

响应式系统如何处理数组、Map/Set 等特殊数据结构?它们的边界与特殊处理是什么?

  • 数组的索引、length 与原型方法
  • Map/Set 的读写方法(get/set/add/has)
  • Vue 2 数组方法的重写 vs Vue 3 Proxy 拦截

数组与 Map/Set 是响应式系统需要特殊处理的数据结构。Vue 2 因使用 defineProperty 无法拦截数组索引赋值与 length 变化,只能重写 push/pop/splice 等数组方法:方法自身触发更新,但直接 arr[i]=xarr.length=0 无法响应,需用 Vue.set/delete。Vue 3 用 Proxy 可拦截数组的 get/set(包括索引与 length)以及 deleteProperty,还能拦截 Map/Set 的 get/set/add/has/delete 等内部方法(因为 Proxy 会拦截这些方法调用,Vue 3 通过拦截集合方法并配合 instrumentation 处理),因此天然支持数组增删改与 Map/Set 的变更。边界与注意:数组的某些操作(如 length 缩短)会触发删除通知;Map/Set 通过方法修改时,Vue 3 需在内部方法上做响应式包装(track 与 trigger);深响应式应为嵌套对象递归代理;对特殊对象(如 class 实例、非普通对象)可能需要 markRaw 或 toRaw 处理。理解这些边界有助于避免"改了数组/集合却不更新"的 bug,并理解 Vue 2 → Vue 3 在数据结构支持上的进步。

本题考察响应式对特殊数据结构的处理。回答要对比 Vue 2 重写数组方法 vs Vue 3 Proxy 拦截索引、length 与 Map/Set 方法,并说明边界与注意事项,体现对响应式覆盖面的理解。