Vue 响应式系统

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

1. Vue 3.5 useId SSR 友好 ID 在水合不一致的工程价值

Vue 3.5 新增的 useId() 组合式函数如何生成 SSR 友好的 ID?它在解决水合(hydration)不一致问题上有怎样的工程价值?

  • useId() 基于组件树位置生成确定性 ID 的实现思路
  • SSR 下服务端与客户端 ID 不一致导致 Hydration Mismatch 的成因
  • 与 useTemplateRef、表单 label/for 关联、无障碍 aria 属性等场景的配合

useId() 是 Vue 3.5 提供的组合式函数,用于生成在服务端渲染与客户端水合时保持一致的唯一 ID。它基于组件在组件树中的位置(父组件递增计数)推导出如 v-0、v-1 形式的字符串:同一个组件实例内多次调用返回稳定值,不同实例间则通过树位置区分,从而保证"同一次渲染在服务端与客户端结果相同"。这从根本上避免了传统 Math.random()、Date.now() 等方案因服务端与客户端分别执行而产生不同 ID 的问题,消除了因 id 属性不匹配触发的 Hydration Mismatch 警告。

水合不一致的本质是渲染过程存在非确定性。useId() 将 ID 生成从"运行时随机"改为"基于组件树位置的确定性推导",因此服务端与客户端输出完全一致。它特别适合表单 label 的 for 关联、aria-controls/aria-labelledby 等无障碍关联、以及需要稳定 ID 的自定义元素场景,既提升水合稳定性又改善可访问性,是 SSR 应用中生成 ID 的标准做法。

<script setup>
import { useId } from 'vue'
const inputId = useId() // 服务端与客户端渲染结果一致,如 v-0
</script>
<template>
  <div>
    <label :for="inputId">用户名</label>
    <input :id="inputId" type="text" />
  </div>
</template>
#
★★★

2. Vue 3.5(latest stable)响应式系统的 release effect 清理与 onScopeStop 在 SSR/异步的工程价值

Vue 3.5 响应式系统中 effect 的自动清理机制是怎样的?onScopeStop() 在 SSR 与异步场景下有什么工程价值?

  • effect 重新执行前对旧依赖的自动清理(release effect)
  • effectScope 停止时通过 onScopeStop() 注册的资源释放回调
  • 异步任务、SSR 渲染流程中避免内存泄漏与重复订阅

Vue 3.5 中 effect(watch、computed、render effect)在每次重新执行前会先"释放"上一次收集的依赖:遍历旧依赖集合移除自身引用,再重新建立订阅,这一机制保证依赖变化后 effect 只保留当前真正依赖的 target/key,避免陈旧引用导致的内存泄漏与多余触发。onScopeStop() 则是 3.5 新增的注册回调 API,在所在 effectScope 被 stop() 时统一触发,用于释放定时器、事件监听、AbortController、WebSocket 连接等非响应式资源,与该 scope 内所有 effect 的级联清理配合,形成完整的生命周期回收。

回答本题要区分两个层面的清理:一是 effect 粒度的"依赖释放",发生在每次依赖变更重跑时;二是 scope 粒度的"整体停止",通过 scope.stop() 触发 onScopeStop 回调。在 SSR 与异步场景中,组件可能未经历完整挂载/卸载流程,onScopeStop 不依赖组件实例、可在纯函数式 effectScope 中使用,因此能精确控制每个渲染作用域的资源生命周期,是防止内存泄漏的关键工具。

#
★★★

3. ref 与 reactive 在解构/展开时的响应性丢失与 toRef/toRefs 的修复

ref 与 reactive 在解构或展开时为什么会丢失响应性?toRef/toRefs 如何修复这一问题?

  • reactive 基于 Proxy,解构后返回原始值从而脱离代理
  • ref 解构同样得到普通值,需要 .value 维持引用
  • toRef/toRefs 创建与源对象保持响应的引用

reactive 返回的是 Proxy 代理对象,解构或展开运算符提取的是目标对象上的原始属性值,脱离代理后对该变量的读写不再经过 Proxy 拦截,因此丢失响应性;ref 的响应性来自其内部 .value 的 getter/setter,直接解构只会复制当前值。toRef(source, key) 返回一个 ref 形式的"属性引用",其 .value 读写始终转发到源响应式对象,因此解构后仍保持同步;toRefs(reactiveObj) 则批量转换整个对象的每个属性,常用于 setup 中 return { ...toRefs(state) } 以在模板中直接使用解构属性而保留响应性。

理解本题的关键是分清"响应性挂在对象上还是变量上":reactive 的响应性在代理对象上,解构切断了与代理的连接;toRef/toRefs 的本质是把"属性访问"延迟到源对象上,形成引用而非复制。工程上组合式函数返回状态时建议用 toRefs 或显式 toRef 包装,避免消费者解构后"静默失效";此外 toRef 对不存在属性也返回可用 ref,便于在动态 key 场景下安全使用。

import { reactive, toRefs, toRef } from 'vue'
const state = reactive({ count: 0, name: 'vue' })
const { count } = state // 失去响应性
const { count: countRef } = toRefs(state) // 保持响应性
const nameRef = toRef(state, 'name')
#
★★★

4. Vue 3 响应式系统的依赖收集(track/trigger)

Vue 3 响应式系统的 track 与 trigger 是如何协同实现依赖收集与派发更新的?在什么时机触发?

  • track 在 get 拦截中收集 effect 依赖(target → key → Set of effects)
  • trigger 在 set 拦截中派发更新并处理数组 length、新增 key 等特殊情况
  • 副作用执行时的 activeEffect 与栈管理

Vue 3 响应式基于 Proxy,读取属性触发 get 拦截进入 track:将当前正在执行的副作用(activeEffect)记录到 target → key → effect 集合 的三级映射中;写入属性触发 set 拦截进入 trigger:取出该 key 对应的 effect 集合并逐一重新执行。执行副作用前会先清空其旧依赖再重新收集,保证依赖精确。trigger 还处理数组方法(push、splice 改变 length)、对象新增/删除属性(需通过 has/ownKeys 拦截收集迭代依赖)等场景,确保 for...of、v-for 等迭代操作能被正确触发。

回答时应抓住"读取收集、写入触发"的主线,并补充两个细节:一是 activeEffect 栈保证嵌套 effect 的正确切换;二是副作用执行前的依赖清理(pre-cleanup)是避免"多余触发"的关键。能进一步说明数组与新增属性的特殊收集路径(iterate key)则体现对响应式实现的深入理解。

#
★★★

5. computed 与 watch/watchEffect 的依赖追踪与副作用清理

computed 与 watch/watchEffect 的依赖追踪方式有何异同?副作用清理(cleanup)如何工作?

  • computed 惰性求值、缓存与"仅依赖变化时重算"的机制
  • watch 显式指定 source 与 watchEffect 自动收集依赖的差异
  • onCleanup 回调在依赖变化或停止时执行

computed 内部基于 effect 实现惰性求值:首次访问时运行 getter 并收集依赖,缓存结果;依赖变化时仅标记 dirty,再次访问才重算,因此它是"按需重算 + 结果缓存"。watch 需要显式传入监听源(ref、getter、reactive 等),仅在源变化时回调;watchEffect 则自动追踪回调内访问的所有响应式依赖,任一变化即重新执行。两者都在副作用回调中提供 onCleanup 注册清理函数,在下一次触发前或组件卸载时执行,用于取消请求、清理定时器;flush 选项控制回调在 pre(渲染前)、post(渲染后 DOM 更新)或 sync(同步)执行。

核心区别在"追踪方式":computed 由读取驱动(惰性、带缓存),watch/watchEffect 由依赖变化驱动(主动执行)。工程上 computed 用于派生状态,watch 用于命令式副作用(DOM 操作、异步请求),watchEffect 适合依赖动态变化的场景。务必掌握 onCleanup 防竞态的用法,例如搜索请求用 abort 旧请求,避免响应乱序。

const query = ref('')
watchEffect(async (onCleanup) => {
  const ctrl = new AbortController()
  onCleanup(() => ctrl.abort()) // 下一次触发或卸载时取消
  results.value = await fetch(`/api?q=${query.value}`, { signal: ctrl.signal }).then(r => r.json())
})
#
★★★

6. ref、reactive、shallowRef、shallowReactive 的语义差异

ref、reactive、shallowRef、shallowReactive 四者在响应式语义上有哪些差异?各自适用什么场景?

  • ref 包装基本类型/对象,模板自动解包,.value 访问
  • reactive 深度代理对象本身
  • shallowRef 只代理 .value 本身,深层不代理

ref 创建一个 { value } 形态的响应式引用,可包裹任意类型,模板中自动解包,JS 中需 .value;reactive 直接深度代理对象,访问属性即为响应式,但局限是只能用于对象类型且无法替换整个引用。shallowRef 仅对 .value 做响应式处理,替换 .value 触发更新,但修改 .value 内部对象的深层属性不会触发——需调用 triggerRef() 手动触发;shallowReactive 只代理对象第一层属性,深层保持原始。四者的区别本质是"代理深度"与"访问形态"的组合。

掌握四者差异的关键是理解"浅响应"为性能优化手段:shallowRef/shallowReactive 适用于大型不可变数据结构(如大列表、图表配置、第三方实例),避免深度代理的开销与意外触发;配合 triggerRef 可手动控制更新时机。日常业务优先用 ref/reactive,性能敏感或数据为"整体替换"形态时用浅响应系列。

#
★★★

7. Vue 3.5 响应式重构后的精确依赖收集与 alien-signals 集成

Vue 3.5 响应式系统重构后实现了怎样的精确依赖收集?与 alien-signals 的集成为后续版本带来什么变化?

  • 3.5 引入基于"版本号 + 双向链表"的依赖追踪,替代 Set 集合
  • 数组索引更新、length 变化等场景的精确触发
  • 依赖链接(Link)结构与 O(1) 的清理

Vue 3.5 对响应式核心进行了重构:以"版本计数 + 双向链表"替换原先的 Set 集合来管理依赖,每个依赖以 Link 节点双向链接,effect 重跑时可在 O(1) 时间内断开并重建依赖链接,从而显著减少收集/清理的分配开销,并使依赖追踪更精确——例如数组索引的直接赋值、length 修改、对象属性增删都能更精准地只触发受影响的 effect。3.6 起 Vue 将 alien-signals(独立的高性能响应式实现)集成作为可选运行时,通过预计算依赖链接、压缩数组等优化进一步降低内存占用与更新开销,同时保持对外 API 兼容。

本题考察对 Vue 响应式演进脉络的了解:从 3.0 的 Proxy + Set 依赖集合,到 3.5 的版本计数 + 双向链表,再到 3.6 集成 alien-signals。回答应强调重构带来的收益(精确触发、O(1) 清理、更少分配),并说明 alien-signals 是"实现替换、API 不变",Vapor Mode 也会复用同一响应式内核,体现对框架底层演进的认知。

#
★★★

8. Vue 3 深度响应(reactive 数组 vs ref 数组)

reactive 数组与 ref 数组在 Vue 3 中的响应式行为有何差异?遍历与替换时各需要注意什么?

  • reactive 数组的深度代理与索引/方法拦截
  • ref 数组需 .value 访问,整体替换触发更新
  • v-for 遍历与模板自动解包下的行为差异

reactive 数组将数组本身深度代理,索引读取、length 修改、push/splice 等变异方法都会触发依赖更新,且能追踪数组元素对象的深层变化;ref 数组的响应性集中在 .value 上:整体替换数组(arr.value = newArr)触发更新,但通过 .value[0].x = 1 修改元素对象深层属性同样响应(因元素对象本身被深度代理)。模板中两者遍历形式相同(v-for 自动解包 ref),但在 JS 中 reactive 数组直接 arr[0] 访问,ref 数组必须 arr.value[0]。替换整个数组时,reactive 数组需借助 splice 等变异方法或整体赋值(会丢失引用,需注意),ref 数组直接替换 .value 即可。

差异根源是"代理的位置":reactive 把代理放在数组对象本身,ref 把代理放在包裹容器上。工程建议:组合式 API 中状态集合用 ref([]) 便于整体替换与 store 分发;局部表格数据用 reactive([]) 配合变异方法。回答时强调模板自动解包对两种形态的抹平作用,以及 JS 逻辑中 .value 的显式访问差异。

#
★★

9. customRef 在防抖搜索与异步校验的工程化应用

customRef 如何通过自定义 get/set 逻辑实现防抖搜索与异步校验?它的工程价值在哪里?

  • customRef(factory) 返回带 track/trigger 的自定义 ref
  • 在 set 中延迟 trigger 实现防抖
  • 异步校验中结合 AbortController 取消过期请求

customRef 接受一个工厂函数,返回 { get, set } 两个自定义方法,工厂参数提供 track 与 trigger 函数,由开发者决定"何时收集依赖、何时派发更新"。实现防抖:在 set 中清除定时器并延迟调用 trigger,让 v-model 等依赖此 ref 的更新被节流;实现异步校验:set 中先 trigger(立即反映输入态),再在异步回调结束后触发校验结果更新,配合 AbortController 取消过期请求避免竞态。工程价值在于把"防抖/校验"封装为可复用 ref,模板与业务代码无需感知内部实现,仍保持 v-model 双向绑定语义。

本题考察对响应式 API 扩展能力的运用:customRef 的本质是把"响应式行为的定义权"交给开发者。回答时应给出防抖与异步校验的完整套路(set 中延迟 trigger;异步中先 trigger 再二次更新),并强调 AbortController 防竞态。能说明其与 useDebounceFn 等 VueUse 工具的关系更佳。

function debouncedRef(value, delay = 300) {
  let timer
  return customRef((track, trigger) => ({
    get() { track(); return value },
    set(v) {
      clearTimeout(timer)
      timer = setTimeout(() => { value = v; trigger() }, delay)
    }
  }))
}
#
★★

10. onWatcherCleanup() 的副作用清理时机

Vue 3.5 新增的 onWatcherCleanup() 用于什么场景?它在副作用清理时机的把握上有什么工程价值?

  • onWatcherCleanup() 只能在 watch/watchEffect 回调内同步注册
  • 在下一次副作用触发前或 watcher 停止时执行
  • 与 watch 回调中 onCleanup 参数的等价关系

onWatcherCleanup() 是 Vue 3.5 提供的 API,用于在 watch/watchEffect 的副作用回调中注册清理函数,且必须在回调执行的同步阶段调用。注册的清理函数会在两种时机执行:该 watcher 因依赖变化即将再次触发时、以及 watcher 被显式停止或组件卸载时。它等价于 watch 回调的 onCleanup 参数,但作为独立可导入的函数,让清理逻辑的注册更显式、可组合(可在回调内再调用辅助函数注册清理)。典型场景是请求竞态控制:每次副作用触发前先取消上一次请求(AbortController),保证只有最新结果生效。

回答本题的重点是"注册时机(同步)"与"执行时机(下次触发前/停止时)"两点,并把 onWatcherCleanup 与 onCleanup 参数的等价性说清楚。能进一步说明它与组件卸载钩子的区别——它绑定的是 watcher 生命周期而非组件生命周期,在手动创建的 watch(如 watchEffect 返回值手动 stop)场景下更精确。

#
★★

11. effectScope 与 getCurrentScope() 的作用域隔离

effectScope 如何实现响应式作用域的隔离与管理?getCurrentScope() 在其中扮演什么角色?

  • effectScope(true) 创建独立作用域,收集内部所有 effect
  • scope.stop() 级联停止内部 watch/computed/effect
  • getCurrentScope() 返回当前所在作用域,用于嵌套判断

effectScope() 创建一个作用域容器,内部通过 watch、computed、watchEffect 或 watchPostEffect 创建的 effect 都会被自动收集;调用 scope.stop() 时,作用域内所有 effect 级联停止并执行各自的清理逻辑,实现"一次停止、整体回收"。getCurrentScope() 返回当前正在执行的代码所处的作用域实例(若在组件 setup 中则为组件关联作用域),常用于检测运行环境或实现嵌套作用域的管理逻辑,例如自定义库中判断是否应创建子作用域。effectScope 的价值在于把副作用生命周期从组件实例中解耦,适用于可插拔模块、非组件工具函数与测试环境。

核心理解是"作用域即副作用生命周期边界":effectScope 让开发者无需依赖组件卸载也能批量停止副作用,对组合式函数库尤为重要(内部创建的 watch 不应泄漏到使用方)。getCurrentScope 提供运行时环境感知能力。回答时建议对比"组件作用域"与"手动 effectScope"的关系,说明组件 setup 内的 effect 本质上挂在其组件作用域上,组件卸载即 stop。

#
★★

12. effectScope 在可拔插功能模块与依赖收集隔离的工程价值

effectScope 在可拔插功能模块与依赖收集隔离方面有什么工程价值?如何用它构建可安全销毁的功能模块?

  • 为每个功能模块创建独立 scope,内部副作用互不干扰
  • 模块卸载时 scope.stop() 一次性回收全部副作用
  • 避免模块间响应式依赖交叉与内存泄漏

可拔插功能模块(如插件、按需启用的页面能力、动态注册的工具)常需要独立创建并销毁自己的副作用。用 effectScope 为每个模块实例创建一个作用域:模块初始化时创建 scope 并在其内注册全部 watch/computed,模块被移除或停用时调用 scope.stop(),该模块的所有响应式订阅与清理回调一次性回收,无需逐个手动停止。依赖收集隔离的价值在于:不同模块的 effect 只订阅各自需要的数据,即使共享全局 store,模块 A 的 effect 不会因模块 B 的私有状态变化而被误触发,更新粒度与回收边界都更清晰。

本题考察 effectScope 的工程化建模能力:把"作用域"映射为"模块实例的生命周期"。回答建议给出模式——createModule() 内部 const scope = effectScope();scope.run(() => { 注册副作用 });返回 { dispose: () => scope.stop() }。强调这对长期运行应用(编辑器、仪表盘、动态路由)避免 retained heap 增长与跨模块泄漏的实际价值。

#
★★

13. Vue 响应式系统的循环引用警告策略与 WeakMap/WeakSet 链路

Vue 响应式系统如何通过 WeakMap/WeakSet 构建依赖收集链路?遇到循环引用时如何处理并给出警告?

  • targetMap 用 WeakMap 映射 target → depsMap(key → effects)
  • WeakMap 弱引用防止响应式对象本身被长期持有
  • 循环引用对象(a.self = a)的代理与依赖收集

Vue 3 用三层结构维护依赖:外层 WeakMap(target → Map)、中层 Map(key → effect 集合)、内层 effect 集合。选择 WeakMap 的关键在于"弱引用":当响应式对象不再被业务引用时,其依赖元数据可被 GC 回收,避免元数据反向持有对象造成泄漏。对循环引用的对象(如 a.child = b、b.parent = a),Proxy 代理本身天然支持——读取循环属性时按 target/key 正常 track,写入时正常 trigger,不会因循环结构死循环;若模板或数据中存在自引用渲染,运行时会在 v-for 等场景给出"检测到循环引用/无限循环"的警告,提示用深度截断(如 JSON 序列化时 replacer)治理。

本题考察响应式数据结构设计:WeakMap 的弱引用特性是"元数据不泄漏"的根基;而循环引用是数据结构问题而非响应式缺陷,代理机制能正确追踪。回答时可补充"同一 target 只代理一次(缓存于 reactiveMap)"避免重复代理循环问题,以及依赖集合去重防止同一 effect 重复订阅同一 key。

#
★★

14. Vue 3.5+ 响应式的内存优化(版本计数/数组监听)与 Vue 2 的差异?

Vue 3.5+ 响应式在内存优化上做了哪些改进(版本计数、数组监听)?与 Vue 2 的响应式实现相比差异在哪里?

  • 3.5 版本计数替代部分依赖存储,降低元数据内存
  • 数组 length 变化与索引监听的精确化
  • 对比 Vue 2 的 Object.defineProperty 逐属性劫持与数组方法重写

Vue 3.5 的响应式重构在内存上主要有两点:一是依赖管理从 Set 集合改为"版本计数 + 双向链表",effect 记录订阅时的全局版本快照与链接结构,减少了大量中间 Set 对象的分配;二是数组监听更精确——对 length 的修改、索引赋值通过版本号比较判断是否真正变化,避免无意义的依赖刷新。与 Vue 2 的差异是结构性的:Vue 2 基于 Object.defineProperty 在初始化时逐属性劫持,无法监听新增/删除属性(需 Vue.set/Vue.delete),对数组通过重写 7 个变异方法实现,且整对象替换、Map/Set 均不支持;Vue 3 基于 Proxy 可在运行时拦截任意属性读写,天然支持增删属性、数组任意索引、Map/Set/WeakMap 等内置集合,无需预处理,代理深度也更彻底。

对比类问题要抓住"机制差异"与"能力差异"两层:机制上 defineProperty 需要提前知道 key(对象定长),Proxy 可动态拦截;能力上 Vue 3 原生覆盖新增属性、数组索引、内置集合。再叠加 3.5 的内存优化(版本计数、链表)说明演进方向。回答时建议明确"3.5 优化的是实现细节,Vue 3 vs Vue 2 是架构差异"。

#
★★

15. Vue 的响应式在 SSR/Hydration 时的注意事项?

Vue 的响应式系统在 SSR 与 Hydration 阶段有哪些注意事项?如何避免服务端与客户端行为不一致?

  • SSR 阶段不产生持续更新的 effect,组件渲染为一次性快照
  • 避免在 setup 顶层执行浏览器专属副作用
  • onMounted 等客户端钩子中才进行 DOM 相关响应式操作

SSR 阶段每个组件只会被渲染一次输出 HTML 字符串,watchEffect、渲染 effect 等在服务端不会持续订阅更新,因此服务端无需也不应维护长期响应式订阅,浪费内存且无意义。注意事项主要有几点:一是不要在 setup/组件顶层访问浏览器专属 API(window、document、localStorage),否则服务端报错或产生不一致输出;二是副作用(事件监听、定时器、第三方实例化)应放入 onMounted 等客户端生命周期钩子;三是渲染输出必须与服务端一致,任何"仅客户端存在"的随机值、时间戳、本地存储驱动的内容都会导致 Hydration Mismatch;四是 SSR 中计算属性等仍正常求值,但要保证纯函数式、无副作用。响应式在 SSR 中主要承担"渲染期间的数据读取"角色,而非"持续更新驱动"角色。

理解 SSR 下响应式的定位变化是关键:服务端是一次性求值,客户端才是持续响应。因此所有"需要响应式驱动更新"的能力都只能在客户端生效,设计上应把数据获取放到 useFetch/useAsyncData 等 SSR 友好层,把 DOM 副作用放到 onMounted。回答时补充"水合一致性"角度:SSR 输出的 HTML 与客户端首次渲染必须逐字一致,否则 Vue 只能丢弃服务端节点重新渲染,造成闪烁与性能损失。

#
★★

16. triggerRef/shallowRef 与大数据列表的渲染优化

triggerRef 与 shallowRef 如何配合优化大数据列表的渲染?手动触发更新的适用边界是什么?

  • shallowRef 避免大对象深度代理的开销
  • 数据整体替换或内部批量修改后 triggerRef 手动派发
  • 配合虚拟列表减少 DOM 节点数量

大数据列表的性能瓶颈常在"深度代理开销"与"过量 DOM 更新"两方面。shallowRef 只代理 .value 这一层:列表数据整体替换时自动触发更新,而列表项内部字段的批量修改(如批量勾选、批量更新状态)不会触发任何视图刷新,避免深度代理拦截每个属性读写带来的开销。当批量修改完成后,调用 triggerRef(ref) 手动派发一次更新,让列表重新渲染。这种"延迟到批量完成再触发"的模式显著减少渲染次数。更进一步,列表本身应配合虚拟滚动(如 vue-virtual-scroller)只渲染可视区节点,让 shallowRef + triggerRef 的更新仅作用于少量 DOM。适用边界:数据形态是"整体替换或低频批量更新"时收益最大;若列表项需要高频、细粒度局部响应,则仍应使用深层响应式或拆分组件订阅。

本题考察"浅响应 + 手动触发"的优化组合拳:shallowRef 解决了代理开销,triggerRef 解决了"批量变更只触发一次"的诉求。回答时要点明适用前提——更新模式是低频批量而非高频局部;并补充虚拟列表作为 DOM 层的配套优化,形成完整方案。

#

17. Vue 3.6 中 alien-signals 与 Vue Vapor Mode 的集成现状(Vapor 仍标记实验性/逐步开放)

Vue 3.6 中 alien-signals 与 Vapor Mode 的集成现状如何?Vapor 为何仍标记为实验性并逐步开放?

  • alien-signals 作为默认集成的高性能响应式运行时接入 Vue 3.6
  • Vapor Mode 复用同一响应式内核、绕过 VDOM 直接生成 DOM 操作
  • 仍处实验/beta 阶段,需 opt-in 开启

Vue 3.6 将 alien-signals 集成进响应式层:它是独立的高性能信号实现(预计算依赖、最小化分配),作为默认内置运行时接入后,响应式更新开销进一步下降,普通 VDOM 渲染与 Vapor 编译产物都受益。Vapor Mode 是"无虚拟 DOM"的编译模式:编译器直接把模板编译成细粒度的 DOM 更新指令,复用同一套响应式内核,因此 alien-signals 的优化同时作用于两条渲染路径。由于 Vapor 会绕过 VNode/组件渲染函数的既有契约(第三方库通过 VNode 检查、slot 克隆等能力会失效),且 Options API 部分能力尚未完全覆盖,官方将其标记为实验性并采用 opt-in + 逐步开放的策略:开发者按组件/页面粒度启用,先验证兼容性再扩大范围,配套提供灰度与回滚路径。

本题考察对 Vue 演进路线的宏观理解:alien-signals 是"内核性能优化",Vapor 是"编译策略变革",二者共享响应式基础。回答需强调 Vapor 实验性的原因——生态兼容(第三方库依赖 VNode API)、能力覆盖不全(Options API)、需要渐进采用,而非技术不成熟。能提到"Vapor 是 opt-in、可混合使用"说明对官方发布策略的理解。

#

18. Vue 3.6 在 alien-signals 重构后,动态分支 effect、嵌套 computed 和数组索引更新分别如何建立与清理依赖链接

Vue 3.6 采用 alien-signals 后,动态分支 effect、嵌套 computed 与数组索引更新分别如何建立和清理依赖链接?

  • 动态分支 effect 的条件变化导致依赖集合整体重建
  • 嵌套 computed 的依赖链与缓存失效传播
  • 数组索引更新的订阅粒度与版本比较

alien-signals 中依赖以"订阅者 → 依赖"的双向链表(Link)组织,建立与清理都是指针操作。动态分支 effect(如 if (flag) 访问 a.value else 访问 b.value)在每次重跑前先沿旧链表断开全部依赖链接,再按当前分支重新收集并链接,保证"只订阅实际访问过的依赖"——这是精确依赖收集对分支变化的自然处理。嵌套 computed 形成依赖链:内层 computed 作为外层 computed/effect 的依赖节点,内层失效时沿链通知外层标记 dirty,读取时才级联重算并重建各自链接;更新采用"按需重算"避免整链重算。数组索引更新:访问 arr[i] 的 effect 订阅的是"数组 + 索引"维度的依赖;直接改 arr[i] = v 时触发该索引订阅者,length 变化则触发迭代类订阅者,版本号比较可跳过未实际变化的更新,链接只需局部断开重连。

本题考察对信号式响应式实现的微观理解。回答应围绕"链表链接 = 订阅关系"展开:动态分支体现"重跑即重建",嵌套 computed 体现"惰性级联失效",数组索引体现"按 key 粒度订阅 + 版本去重"。理解三者共同点是"每次更新都先清理旧链接再按需建立新链接,保证订阅精确、无陈旧引用"。

#

19. Pinia 与组合式 API 的状态设计最佳实践?

使用 Pinia 与组合式 API 时,状态设计有哪些最佳实践?如何组织 store 与组件状态?

  • setup store 与 options store 的选择
  • store 内 state 保持扁平、可序列化
  • getters/actions 的职责划分与组合

Pinia 状态设计最佳实践:优先使用 setup store(组合式写法),与组合式 API 心智一致,便于复用逻辑;state 尽量扁平、只放可序列化数据,复杂对象(组件实例、DOM、类实例)放不入 state 或存入非响应式字段;派生数据用 getters 而非在组件中重复计算,需要传参的派生用返回函数的 getter;异步与业务操作放入 actions,action 内部可调用其他 store 的 action 完成协作;组件内只通过 store 暴露的 ref 读取状态,修改一律经由 action 保证变更收敛。边界划分上:跨组件共享、需要持久化或全局一致的数据进 store,仅单个组件或局部 UI 使用的状态留在组件内(ref/computed),避免 store 膨胀;多实例场景(SSR、微前端)用 pinia 实例隔离并通过 defineStore 的 id 区分。

本题是开放型最佳实践题,回答应体现"分层"思想:state 最小化且可序列化、getters 负责派生、actions 收敛变更、store 与组件状态按共享范围划分。结合组合式 API 特点说明 setup store 的现代推荐地位,并提及 SSR/测试场景下 Pinia 实例隔离与 store 复用(useStore 注入 pinia 实例)的注意事项。