Signals 与细粒度响应式(跨框架范式)

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

1. TC39 Signals 提案(Stage 1)的 Signal.State/Computed/Effect 设计

TC39 Signals 提案(已进入 Stage 1)设计了哪三个核心原语?它们的语义与相互约束关系是怎样的?

  • Signal.State(可读写状态单元)、Signal.Computed(派生值)、Signal.effect(副作用)三个原语
  • 拉取式求值、惰性 computed 与依赖追踪的语义
  • 三原语的关系:computed 依赖 state、effect 订阅依赖图

TC39 Signals 提案定义三个核心原语:Signal.State 是可变状态单元,持有一个值并支持 get/set(set 触发依赖失效);Signal.Computed 是派生计算值,基于其他 signal 计算、惰性求值并缓存(只有在被读取时才计算,依赖变化后标记失效、下次读取时重算);Signal.effect 是副作用函数,显式订阅其读取的所有 signal,依赖变化时自动重新执行(微任务中调度、批量去重)(注:官方提案目前仅包含 Signal.State、Signal.Computed 与底层 Signal.subtle,effect 由 signal-polyfill 提供,属 polyfill 层能力)。三者构成响应式图:State 是叶节点,Computed 是派生节点(只读视图、可组合),effect 是图的"输出端点"。

设计要点:一是"拉取式(pull-based)+ 脏标记"的求值模型——computed 只在被读取时按需重算,避免无谓计算;二是依赖收集是动态的(执行时记录读取的 signal,而非声明式声明),因此条件分支中的依赖变化也能被正确追踪;三是 effect 对同一轮变更批量执行一次(去重),且写入 effect 内的 signal 会触发新的调度(避免死循环有额外约束)。三原语的约束关系:computed 只能读其他 signal(不能直接写 State,保证派生可重放)、effect 可读写并产生外部副作用;提案目标是为框架提供统一底层,各框架可在其上实现自己的响应式 API。

先逐个讲清三个原语的语义(State 可变、Computed 惰性缓存、effect 副作用订阅),再讲它们构成响应式图的关系与"拉取式求值 + 动态依赖收集 + 微任务批量"三个设计机制,即完整覆盖提案核心。

#
★★

2. Signals 与虚拟 DOM diff 的边界,何时仍需组件级更新

细粒度响应式(Signals)已经能精确更新单个 DOM 节点,虚拟 DOM diff 还有存在的必要吗?两者协作的边界在哪里?

  • 细粒度更新精确到节点 vs diff 需要整棵树参与
  • 组件级更新的必要性:列表结构变化、插槽/子树结构变化
  • Signals 更新数据 vs 虚拟 DOM 更新结构的分工

Signals 的价值在"数据驱动的值更新":某个状态变化时,只更新读取该状态的 DOM 节点(如一个文本、一个 class),不重建整个组件,这在"值级"更新上效率远超虚拟 DOM diff。但虚拟 DOM 的核心价值在于"结构更新":当子树结构变化(列表增删、条件分支切换、children 替换)时,diff 能精确计算"哪些节点增删移动"并最小化 DOM 操作,而纯 Signals 方案面对结构变化仍需框架做"创建/销毁/移动"的协调,此时虚拟 DOM(或等价的结构 diff 机制)依然有存在意义。

边界划分:数据值高频变化的场景(实时数值、输入、进度、协作光标)用 Signals 细粒度更新收益最大;结构频繁变化的场景(动态列表、插槽、路由切换的整块内容替换)仍依赖组件级/结构级协调,虚拟 DOM diff 提供声明式的结构同步;两者可分层协作——外层组件树与结构用虚拟 DOM(或 Solid 式的编译期结构控制),内层高频值的更新用 Signals 精确订阅,避免"值变了整组件重渲染"。工程上"何时仍需组件级更新"的判据是:变化是否改变组件树的形状;只改值 → Signals 直达;改形状 → 需要结构协调机制。

答题关键是区分"值更新"与"结构更新"两类变化:Signals 解决前者,diff/结构协调解决后者,二者是互补而非替代。给出判据(是否改变组件树形状)即可把边界讲清楚。

#
★★

3. Signals 与 Virtual DOM 的更新机制对比,什么场景下 Signals 收益最大?

对比 Signals 与 Virtual DOM 的更新机制,在什么场景下 Signals 的收益最大?各自的成本在哪里?

  • Virtual DOM:render 整组件 + diff + patch,成本与组件规模相关
  • Signals:依赖订阅直达更新节点,成本与"变化量"相关
  • 收益最大场景:高频值更新、大组件树中小状态变化、实时数据流

虚拟 DOM 的更新机制是"组件重渲染 + 虚拟树 diff + patch 真实 DOM":状态变化触发组件 render 生成新虚拟树,与旧树 diff 出差异再最小化更新 DOM,成本随组件树规模与 diff 范围增长(即便只改一个值也要走完该组件的 render-diff 链路,配合 memo 等优化才能裁剪)。Signals 的更新机制是"依赖订阅 + 直达更新":状态变化通知订阅者(computed/effect),只执行与变化相关的读取节点更新,不重跑无关组件逻辑,成本与"实际变化量"成正比,不随组件规模增长。

收益最大的场景:一是大组件树中只有小块状态频繁变化(如大型表单、编辑器属性面板),虚拟 DOM 会重渲染整个子树而 Signals 只更新绑定节点;二是高频值流(实时行情、输入联想、协作光标、动画数值),每秒几十上百次更新时 diff 开销被放大;三是嵌套深层的数据读取(深对象属性更新),Signals 按依赖直达避免逐层重渲染。成本对比:虚拟 DOM 的隐性成本是 render 与 diff(可用 memo/编译优化缓解),Signals 的成本是每个订阅节点的创建与依赖追踪内存开销、以及手动管理订阅的生命周期。选择上:中小应用虚拟 DOM 的心智与生态更成熟,超高频/超大树场景 Signals 系(Solid、Preact Signals、Vue 响应式)收益显著。

对比框架是"成本随规模"vs"成本随变化量":虚拟 DOM 重渲染与 diff 是固定链路,Signals 直达订阅节点。能给出"高频、大组件树、局部小状态"三类收益最大场景并指出各自成本,即完整。

#
★★

4. 原生 Signals 与现有框架响应式系统的互操作路径

TC39 原生 Signals 与现有框架(Vue/React/Solid)的响应式系统之间如何互操作?有哪些可行的接入路径?

  • 原生 Signals 作为通用底层的定位
  • 接入路径:包装、转换、桥接 effect 与框架更新循环
  • 双系统并存的同步与生命周期问题

原生 Signals 的目标不是取代框架响应式,而是成为"标准底层",框架通过互操作层接入,主要路径有三:一是包装(wrap)——框架把自己的响应式 API 内部实现替换为原生 Signals(如 Vue 的 ref 底层改用 Signal.State,Solid 的 createSignal 映射到原生信号),对上层 API 不变,框架获得标准化底层与跨框架共享能力;二是桥接(bridge)——框架不替换内部实现,而是把原生 signal 读入框架状态(如 effect 读取原生 signal 后 setState/触发组件更新),或把框架状态导出为 signal 供外部读取,适合渐进接入存量代码;三是适配(adapter)——提供通用工具库(如 signal-utils)做双向同步:原生 signal 变化 → 框架更新,框架状态变化 → 写入原生 signal。

互操作的关键是"同步边界":原生 signal 与框架状态之间要保持一致性,需明确单向数据流(避免双向同步死循环)、统一调度(原生 effect 的微任务调度与框架批处理机制对齐,避免中间态泄漏)与生命周期(signal 订阅随组件销毁而清理)。实践中优先"框架内部换底层"或"边界处单向桥接"两种模式,避免在组件内大量手工同步;TC39 提案也定义了下层适配(如 Signal.subtle)供框架接入时使用。

按"包装/桥接/适配"三条路径讲互操作,再落到同步边界三要素(单向流、调度对齐、生命周期清理),即可覆盖。突出"标准底层 + 框架接入"的定位而非竞争关系是理解关键。

#
★★

5. 异步 Signal(async computed/loading 态)的提案进展,Suspense 集成与竞态处理

Signals 如何表达异步计算(async computed)与 loading 态?TC39 提案在 Suspense 集成与竞态处理上有哪些进展?

  • 异步派生值的状态建模(pending/success/error)
  • 与 Suspense 集成:让 computed 可"挂起"等待数据
  • 竞态处理:过期结果丢弃、最新请求优先

同步 computed 无法表达异步结果,社区方案(如 signal-utils 的 asyncComputed、Solid 的 createResource)把异步派生值建模为状态机:pending(加载中)→ success(成功值)或 error(失败),并暴露 isLoading/error/retry 等派生状态;底层通过 promise 与 signal 封装(如 signal.from(promise)),promise resolve/reject 后写入状态 signal,使 UI 能以声明方式渲染 loading/成功/错误三态。TC39 提案层面,异步 signal 的进展方向是把 promise 纳入响应式图:computed 读取异步 signal 时产生"挂起"语义,与框架的 Suspense 集成——框架遇到挂起值暂停渲染等待 resolve,实现"async computed + Suspense"的统一体验,同时保持响应式(数据变化重新挂起/更新)。

竞态处理是异步响应式的核心难点:多个异步请求交错返回时,旧请求的结果可能覆盖新请求(用户快速切换参数),需要"过期丢弃"机制——每次请求记录代次(generation),返回时仅当代次最新才写入;实现上 asyncComputed 依赖变化时取消/忽略旧 promise、以最新依赖的重算为准,并支持 AbortSignal 取消请求。设计上还要求错误可恢复(重试)与请求去重(相同依赖不重复请求)。工程建议:把"异步数据获取"封装成 resource 原语,统一 loading/error/竞态语义,业务代码只消费状态。

分三段:异步值的状态建模(pending/success/error)、Suspense 集成(挂起语义与统一渲染)、竞态处理(代次校验与取消)。能讲清"状态机 + 挂起 + 代次"三要素即完整。

#
★★

6. Vue 3.5+ 的响应式重构与 Vapor Mode 对 Signals 的借鉴

Vue 3.5+ 的响应式系统做了哪些重构?Vapor Mode(无虚拟 DOM 编译模式)从 Signals 借鉴了什么?

  • Vue 3.5 的响应式性能改进(版本计数、内存优化、watch 调度)
  • Vapor Mode:编译期生成细粒度更新代码、无虚拟 DOM
  • 对 Signals 范式(细粒度、编译期依赖)的借鉴与差异

Vue 3.5+ 对响应式系统的重构集中在性能与内存:引入"版本计数(version counter)"优化依赖追踪与批量触发、改进数组与集合的响应式处理、优化 watch/computed 的调度与去重、降低系统开销(减少内存占用、提升大型响应式应用的触发效率),同时补充了 useTemplateRef、watchEffect 改进等 API 演进,让 Vue 的响应式在大型应用上更接近"原生 signals 级"的效率。

Vapor Mode 是 Vue 的实验性无虚拟 DOM 编译模式:编译器把模板直接编译为"细粒度更新代码"——每个绑定位置(文本、属性、class)生成独立的更新函数,状态变化时只调用对应更新函数,跳过虚拟 DOM 生成与 diff,这正是对 Signals 范式的借鉴:把"运行时的依赖追踪 + 节点级精确更新"前移到编译期(编译期静态分析依赖,运行时只做最小更新),在保留 Vue 模板心智的同时获得类似 Solid 的更新效率。差异在于:Vue 的响应式仍是运行时 Proxy 追踪,Vapor 的依赖分析依赖编译期模板信息;Vapor 目前与虚拟 DOM 模式并存、按组件/应用级选择,生态兼容(组合式 API 与响应式 API 保持一致)。方向意义:主流框架都在向"细粒度、无 diff"收敛,Signals 作为标准底层将加速这一演进。

分两层:Vue 3.5 的运行时响应式重构(版本计数与调度优化),Vapor Mode 的编译期细粒度更新(无虚拟 DOM)及其对 Signals 的借鉴。点出"运行时追踪 vs 编译期分析"的差异与演进方向即可。

#
★★

7. 如何定位响应式系统的「过度计算」与「无效依赖」

在 Signals/响应式系统中,"过度计算"与"无效依赖"分别指什么?如何定位与修复它们?

  • 过度计算:computed/effect 执行次数超过必要(重复计算、全量重算)
  • 无效依赖:订阅了未实际影响输出的依赖(过度订阅导致误刷新)
  • 定位手段(Devtools 追踪)与修复策略(拆分粒度、条件订阅)

过度计算指计算节点(computed/effect)的执行频率或范围超过实际需要:典型成因包括 computed 内部读取的依赖粒度过粗(读整个对象导致任意字段变化都重算)、计算中包含不必要的高频依赖(如读取时钟)、effect 在无关变化时重复执行、以及链条过长导致的级联重算。无效依赖指依赖图中订阅了"对结果无实际贡献"的节点:例如在 computed 中读取了从不参与结果的 signal、或在条件分支外的多余读取,导致该 signal 变化时触发无意义的失效与重算(false invalidation),让订阅者被"伪更新"。

定位手段:一是借助响应式调试工具(Solid Devtools、Vue Devtools 的 effect 追踪)查看每个 computed/effect 的依赖列表与执行次数,找出"执行次数异常"与"依赖与实际使用不符"的节点;二是隔离法:注释/分支测试定位触发链,或用装饰日志包裹 computed 记录每次重算的原因;三是静态审查依赖访问路径(在条件外读取、在大对象上点读)。修复策略:拆分粒度(把大对象拆成多个原子 signal、computed 只读所需字段)、收敛依赖(computed 内只读取参与计算的依赖、避免副作用与随机读取)、条件化订阅(把高频依赖移出热点计算)、缓存中间结果(用 memo 化的派生层避免重复计算),以及使用 equals 相等判定过滤"值未变"的刷新。核心原则:让每个 computed/effect 的依赖集合恰好等于其实际输入。

先分别定义两个问题(执行过多 vs 订阅多余),再讲定位工具与修复手段,最后落到"依赖集合=实际输入"的黄金准则。能举例说明"读整个对象导致过度重算"即体现经验。

#
★★

8. Signal 图的循环依赖检测与防护

Signal 响应式图中为什么会出现循环依赖?如何检测与防护?

  • 循环依赖的成因(computed 依赖自身/相互依赖、effect 写回读)
  • 检测机制:依赖图拓扑分析、运行期调用栈与重入检测
  • 防护:报错提示、读取保护与设计约束

循环依赖指依赖图出现环:常见成因有三——computed 直接或间接读取自身(A 依赖 B、B 依赖 A,或 computed 内读取自己)、effect 写入自己正在读取的 signal(写回读形成环)、以及推导链中的意外自引用(如通过全局状态间接引用自身)。出现环后,惰性求值的 computed 会陷入"标记-求值"的反复(每次求值都因依赖未定而再次失效),effect 可能无限触发,导致栈溢出或卡死。

检测与防护分三层:一是静态/拓扑层——框架在依赖图维护时检测"新增依赖是否与当前求值链构成环"(如 Solid/Vue 在依赖收集中记录当前活跃的求值栈,发现把"栈中节点"加入依赖即判定环);二是运行期防护——遇到环时报错("circular dependency")并中断求值,或对重入做深度限制,effect 通过调度去重避免同步死循环;三是设计约束——computed 保持纯函数(只读不写),避免在 computed 内产生副作用或通过全局可变状态间接自引用,effect 的写入若与读取重叠应重构为"先算后写"或拆成两个 effect。工程上还应提供依赖图可视化工具帮助定位环的路径,并在 CI/运行时做环检测告警。

先讲三类成因(自读、相互依赖、写回读),再讲"求值栈活跃节点检测"的机制与运行期报错防护,最后落到 computed 纯函数的设计约束,即完整。

#
★★

9. 细粒度响应式与不可变数据(Immer)的协作与冲突

细粒度响应式(Signals)与不可变数据(Immer)如何协作?它们之间有什么冲突?各自适合什么场景?

  • 不可变数据 + 引用比较与响应式订阅的结合
  • Signals 的嵌套/字段级追踪与不可变结构的差异
  • 冲突点:路径更新 vs 结构共享、浅比较订阅 vs 深层监听

不可变数据的核心价值是"结构共享 + 引用相等判断":每次修改产生新引用,旧引用不变,框架可用引用比较(===)快速判断状态是否变化并裁剪更新(React 的 reducer/selector 依赖此机制);Signals 的核心价值是"字段级订阅 + 直达更新":订阅某个 signal 的节点只在它变化时更新。协作模式:把不可变状态整体装进一个 signal,每次产生新状态 set 进去,依赖该 signal 的 computed/effect 用引用比较做"值未变则跳过"的过滤(配合 equals 相等判定),既享受不可变的可预测性,又避免整树重算;Immer 的 produce 产生的新 draft 天然适配这种"一次 set、引用级 diff"的流程。

冲突点:一是追踪粒度——Signals 若在嵌套对象上做字段级追踪(如 Vue 的响应式深代理),与不可变的"整体替换"语义有张力:深代理会拦截每次访问并建立细粒度订阅,而不可变风格是一次性替换整个状态,两者的订阅体系会冗余(订阅了旧对象的节点需要跟随引用迁移);二是路径更新——不可变数据用"路径更新"(更新深层字段),Signals 的字段级追踪期望"访问即订阅",深度访问路径上的中间对象变化也会触发失效,可能造成过度订阅;三是调试心智——不可变数据强调快照回放,Signals 强调实时流,混用需明确"以谁为准"。实践建议:状态边界处用不可变(跨组件传递、Redux 风格)、热点内部用细粒度(局部高频值),用 equals/浅比较桥接二者,避免在不可变大对象上做深代理级订阅。

从"引用比较"与"字段订阅"两种机制讲协作(整体装 signal + 引用相等过滤)与冲突(粒度、路径、心智),最后给出分层使用建议,即完整覆盖。

#
★★

10. 在大型表格/画布场景中 Signals 的渲染优化实证

在大型表格、画布等高频渲染场景中,Signals 相比传统方案(如 React + 虚拟 DOM)有哪些可量化的渲染优化?实践中如何验证与落地?

  • 高频更新下 diff/重渲染成本 vs 细粒度直达更新的实证对比
  • 大型表格/画布的更新模式(单元格高频值、局部重绘)
  • 落地方式与基准验证方法

大型表格(如数据网格)与画布(如编辑器、监控大屏)的核心痛点是"局部高频更新 + 整体节点量大":传统虚拟 DOM 方案中,某单元格数值每秒更新多次,会触发所在组件(甚至整行/整表)的 render-diff 链路,节点量越大浪费越明显;Signals 方案中,单元格文本节点直接订阅对应数据 signal,数据变化只执行该文本节点的更新函数,更新成本与"变化单元格数量"成正比,与表格总节点数无关。社区实证(如 Solid、Preact Signals 的基准)显示:在数千行表格 + 高频值更新场景,细粒度方案的首批更新与持续更新吞吐明显优于 diff 方案,且内存占用更可控(无虚拟树对象)。

实践中验证与落地:一是基准先行——用同一数据集分别实现/对比(如 5000 行 × 高频刷新、画布 1000+ 对象的属性面板实时联动),记录首帧、持续 FPS、单次更新耗时、内存曲线,量化收益;二是按更新模式选型——值级高频更新(实时行情、协作光标、拖拽数值)用细粒度,结构级变化(行列增删、排序)保留结构协调;三是工程化配套——虚拟滚动/回收仍然必要(控制节点总量)、更新合并(微任务批处理避免单次高频同步刷爆渲染)、canvas 场景中 signal 驱动"脏区重绘"而非整帧重绘。结论是"细粒度响应式在高频值场景收益可量化、可基准验证,但结构管理与渲染预算(虚拟化、批处理)仍是工程必需"。

答题主线:量化对比(成本随变化量 vs 随节点量)→ 实证方式(基准设计与指标)→ 工程落地(虚拟化、批处理、脏区重绘)。能说出"更新成本与变化量成正比"的量化逻辑即抓住本质。

#
★★

11. 原生 Signal 作为底层、框架响应式作为上层的分层架构

"原生 Signal 作为底层、框架响应式作为上层"的分层架构是什么?各层承担什么职责?这种架构有什么价值与风险?

  • 分层职责:底层原生 Signals 负责依赖图与更新调度,上层框架负责组件/模板语义
  • 价值:标准统一、跨框架互操作、心智收敛
  • 风险:抽象泄漏、双语义并存的成本

分层架构中,底层是 TC39 原生 Signals:提供 State/Computed/effect 与依赖图、调度、相等判定等通用机制,不感知组件、模板或渲染,是"纯粹的状态与依赖计算层";上层是框架响应式:负责组件生命周期、模板编译、渲染与 DOM 更新,把框架语义(如 Vue 的 ref/computed、Angular 的 signals、Solid 的 createSignal)映射到底层原生信号上,或用桥接层读取原生信号驱动框架渲染。价值:一是标准统一——跨框架共享同一套依赖图语义,开发者心智收敛,库与工具(Devtools、调试、序列化)可复用;二是互操作——不同框架、非框架代码(普通 JS 模块)之间可以共享 signal 数据;三是演进——框架专注于渲染语义,状态底层随标准演进受益。

风险与成本:一是抽象泄漏——框架需要暴露"信号 vs 框架状态"两个概念,用户被迫理解双层语义,API 复杂度上升;二是调度语义对齐——原生 effect 的微任务调度与框架的批处理/渲染时机需严格对齐,错位会产生中间态闪烁或额外渲染;三是性能边界——底层通用性意味着框架无法做完全定制的优化(如编译期依赖分析),性能天花板可能低于框架自研(如 Vapor);四是迁移成本——存量框架内部实现替换需要重写响应式内核。实践中分层要"适可而止":标准底层解决互操作与共识,框架层保留自研空间(如 Vue 仍可做编译期优化),通过适配层双向桥接而非强制全部替换。

先定义两层职责(底层状态依赖 vs 上层渲染语义),再讲价值(统一、互操作、演进)与风险(抽象泄漏、调度对齐、性能天花板),最后给"适配桥接而非全盘替换"的实践建议,即完整。

#
★★

12. Signal 的只读视图(Signal.readonly)与封装性

Signal.readonly 的作用是什么?只读视图在状态封装与数据流设计上有什么价值?

  • Signal.readonly 提供同一状态的只读视图(类型层面防写)
  • 封装性:内部可写、外部只读的模块边界
  • 数据流设计:单向流、防滥用、调试与安全

Signal.readonly(signal) 返回同一个状态的只读视图:读取行为完全一致(get/订阅),但类型上禁止 set,调用 set 会报错(运行时也可实现保护)。它解决的是"类型与语义层面的封装":状态在内部(如 store 模块、组件内部)可写,导出给外部(其他模块、跨组件)时用 readonly 包装,外部代码在编译期/类型检查期就无法误写,把"该状态只能由谁修改"写进类型系统,避免依赖口头约定。

价值体现:一是模块边界——store 的创建者持有可写 signal,消费者只拿只读视图,实现"单一写入者"的单向数据流,防止散落各处随意改状态导致的不可预测性;二是组件 API 设计——组件接收 props 中的 signal 用 readonly 类型,内部自己创建的可写 signal 不外泄,强化"数据向下、事件向上"的约定;三是安全与调试——外部只读使状态变更路径收敛,便于追踪谁在写(变更只发生在持有者处),也避免第三方库误写你的状态;四是组合性——computed 本身已是只读派生值,readonly 让"可写叶子"也能以只读身份参与组合,统一了"读"的接口。注意 readonly 是"视图"而非拷贝:内部写后外部读到新值,适合共享状态场景;需要快照时用 signal.get() 取瞬时值。

先讲 readonly 的语义(同一状态的只读视图,类型禁写),再从模块边界、组件 API、调试安全、组合性四个价值展开,最后澄清"视图非拷贝"的语义,即完整覆盖封装性考点。

#
★★

13. Signals 在服务端(SSR)的水合对齐问题

Signals 在服务端渲染(SSR)场景会遇到哪些水合(hydration)对齐问题?如何解决?

  • SSR 双份执行(服务端渲染 HTML、客户端水合)导致的信号状态不一致
  • 水合对齐问题:状态快照、订阅时机、计算差异
  • 解决方案:状态序列化传递、避免水合期副作用、双端求值一致

SSR 中同一组件在服务端与客户端各执行一次:服务端把信号状态渲染成 HTML,客户端水合时重新创建信号并接管 DOM。对齐问题有三类:一是状态不一致——服务端与客户端创建信号时的初始值不同(如依赖 Date、随机数、浏览器全局),导致水合后 DOM 与客户端信号值不一致(mismatch 警告或闪烁);二是订阅时机——服务端渲染是"一次性求值"(拉取数据后渲染 HTML,effect 不应在服务端执行副作用),客户端水合后才建立订阅,若 effect 在服务端被错误执行或水合前执行,会污染状态或执行双份副作用;三是计算差异——computed 依赖平台差异(window、localStorage)时服务端与客户端结果不同。

解决方案:一是状态序列化传递——把服务端计算出的信号值序列化进 HTML(如 INITIAL_STATE 脚本),客户端用该快照初始化信号,保证双端初始一致;二是约束服务端执行——effect 只在客户端执行(服务端跳过副作用),SSR 期间用"一次性求值"而非持续订阅,避免服务端产生响应式开销与副作用;三是保证求值确定性——把平台相关值延迟到客户端读取,computed 避免直接访问浏览器全局(通过注入/条件判断),保证双端 computed 结果一致;四是水合时建立订阅——客户端从快照创建信号后立即建立依赖订阅并做一致性校验(发现不匹配走客户端重渲染)。Solid/Preact Signals 等方案的 SSR 实践已把"服务端快照 + 客户端接管"作为标准模式。

按"双份执行"的根源讲三类对齐问题(初始状态、订阅/副作用时机、双端计算差异),再对应三类解决方案(快照传递、客户端才订阅、确定性求值),即构成完整答案。

#
★★

14. 如何在 React 中通过 polyfill 使用 TC39 原生 Signals

在 React 中如何通过 polyfill 使用 TC39 原生 Signals?与 React 状态的协作模式与限制是什么?

  • polyfill 的引入与原生信号的使用(signal 包 / 框架适配)
  • 协作模式:useSignal/useEffect 订阅、外部 store 桥接
  • 限制:React 渲染调度与信号更新的桥接成本、并发特性

在 React 中使用 TC39 原生 Signals 需要 polyfill(如 @preact/signals-react、signal-polyfill 或标准参考实现):polyfill 提供 Signal.State/Computed/effect 的实现(浏览器未原生支持时用 JS 模拟依赖追踪),配合 React 适配层(如 Preact Signals React 集成、useSyncExternalStore 桥接)才能让信号变化触发 React 组件更新——因为 React 组件本身不是响应式容器,需要把信号读取"翻译"成 React 的渲染订阅。

协作模式主要有三种:一是 Hook 适配——组件内用 useSignal()/useComputed() 读取信号,适配层在渲染期间读取并注册订阅(基于 useSyncExternalStore 或自研订阅 + 版本号),信号变化触发组件重渲染,实现"信号作为外部状态源";二是细粒度桥接——对高频非 UI 逻辑(图表数据、动画参数)直接用 effect 订阅信号做 DOM/实例级更新,绕过 React 渲染;三是全局 store——把 React 状态管理(Redux 等)与信号互操作:信号写 store 或 store 导出为信号,注意保持单一数据流。限制:一是 React 的渲染是"整组件重渲染",信号的高频变化若每次触发重渲染,细粒度收益被 React 渲染模型抵消(这正是 Preact Signals 做编译器优化/上下文优化的原因),高频场景建议绕过 React 渲染;二是 React 并发渲染(Suspense、transition)与信号订阅的调度需要适配层处理一致性,避免渲染中间态读到不一致信号值;三是 effect 在组件卸载时的清理要正确(避免泄漏与更新已卸载组件)。

答题主线:polyfill 与适配层(React 非响应式容器 → 需要桥接)→ 三种协作模式 → 限制(整组件重渲染 vs 细粒度、并发调度、生命周期清理)。理解"桥接本质"即可得分。

#
★★

15. Signals 与 Angular/Vue/Solid 类型系统的统一尝试

Signals 与 Angular、Vue、Solid 各自的类型系统如何统一?统一类型系统的尝试面临哪些问题?

  • 各框架信号 API 的类型差异(Signal 、Ref 、Accessor)
  • 统一尝试:通用 Signal 类型接口、兼容层与类型别名
  • 统一面临的问题:语义差异、逆变/协变、工具链分歧

各框架的信号类型各不相同:Solid 用 Accessor (读取函数,T 直接为值类型)、Angular 用 Signal (带 set/update 方法)、Vue 用 Ref (.value 属性访问,深层解包)、Preact Signals 用 Signal (.value)。统一尝试主要有三:一是定义通用信号接口——按 TC39 提案语义定义统一类型(如 WritableSignalLike 暴露 get/set,ReadonlySignalLike 只读),各框架类型通过适配器满足该接口,让跨框架库能以统一类型消费信号;二是类型别名与工具层——signal-utils 等库提供 toSignal/fromSignal 转换与通用工具(combine、memo 等),类型层面用泛型桥接各框架 API;三是编译器/插件层统一——通过类型声明文件的"规范化"让框架信号的类型形状(读/写/订阅)趋同。

面临的问题:一是语义差异——"读"的表示(函数调用 vs .value 属性)与"写"的能力(set/update/mutate)在各框架不同,统一接口只能取最小公分母,丢失框架特色(如 Vue 的深响应式解包、Angular 的 required 输入语义);二是类型系统能力——联合类型、泛型约束、逆变/协变在读写双接口下易出问题(如 ReadonlySignal 与 Signal 的赋值兼容),需要显式类型转换;三是工具链分歧——各框架的 Devtools、类型检查插件、编译宏(Vue 的 ref 宏、Solid 的编译优化)基于自有类型,统一后这些工具需要适配;四是演进冲突——框架 API 演进(如 Vue 3.5、Angular 19)与 TC39 提案演进需同步,防止类型漂移。结论:统一尝试的现实形态是"接口兼容层 + 转换工具",而非消灭框架差异。

先列举三类信号类型差异(Accessor/Ref/.value),再讲三类统一尝试(通用接口、转换工具、类型规范化),最后展开语义与工具链层面的问题,即可覆盖。

#
★★

16. Signal 与 React 状态、Vue ref、Solid store 的语义对齐与差异

TC39 Signal 与 React 状态、Vue ref、Solid store 在语义上有哪些对齐与差异?理解这些差异对跨框架开发有什么意义?

  • 各状态原语的读写模型差异(可变 vs 不可变、值 vs 代理)
  • 更新触发与订阅机制的差异
  • 跨框架协作时的心智对齐

对齐点:它们都是"状态容器",都支持读取当前值、写入新值、并在变化时通知依赖方(订阅者/渲染器),都提倡"状态提升与单一数据流"的工程实践。差异点:React 状态(useState)是"不可变 + 函数式更新"——setState 传新值或更新函数、渲染期快照语义(闭包捕获旧值)、无显式订阅 API,变化通过调度重渲染传递;Vue ref 是"可变 + 代理式深响应式"——.value 读写、深层对象自动响应、依赖收集发生在模板/effect 访问时;Solid store 是"细粒度 + 访问器函数"——createSignal 返回 [get, set] 对、store 用代理按路径读写(store.list[0].name)、更新精确到读取路径;TC39 Signal 是"显式订阅 + 值语义"——get/set/effect 显式建模,是通用底层,不绑定组件模型。

差异的本质影响:一是"读"的时机——React 在渲染函数中读、Solid 在细粒度表达式中读、Vue 在模板编译期读,决定了"谁的更新被追踪";二是"写"的形态——setState 的不可变更新与 signal.set 的可变覆盖在迁移时需转换(用 produce/结构共享桥接);三是订阅粒度——React 整组件、Vue 组件级自动追踪、Solid 表达式级、Signal 精确到节点,粒度差异决定优化手段;四是心智模型——React 的"渲染即快照"、Vue 的"自动追踪"、Solid/Signals 的"显式依赖",迁移时最容易踩的坑是把 React 的不可变思维套在可变信号上(或反之)。跨框架开发的意义:把"状态原语"作为沟通契约,团队在组件边界统一用"只读接口 + 显式更新"约定,底层实现可替换。

先给对齐点(状态容器、读写、通知),再逐框架对比差异(不可变快照 vs 值代理 vs 访问器),最后落到"读的时机/写形态/订阅粒度/心智"四个迁移要点,即完整。

#
★★

17. Signals 的「拉取(lazy)」计算与响应式图(dependency graph)更新

Signals 的"拉取式(lazy)"计算是什么?响应式图在更新时如何工作(脏标记、传播顺序)?

  • 拉取式求值:computed 按需计算、结果缓存
  • 更新流程:状态 set 标记脏 → 传播(propagation)到依赖者 → 读取时重算
  • 惰性 vs 急切(eager)传播的设计取舍

拉取式(lazy/pull-based)求值指 computed 不随依赖变化立即重算,而是先被标记为"脏(dirty)",等到有人读取它时才执行计算并缓存结果;未被读取的派生值即使依赖变化也不产生计算开销。流程:State.set 时框架沿依赖图把直接/间接依赖该状态的 computed 标记为 dirty(传播阶段只做标记、不做计算),effect 会被调度到微任务待执行;当代码读取某个 computed 时,若它是 dirty 则重新求值(并递归检查其依赖是否脏),求值结果缓存,同轮次后续读取直接命中缓存——这就是"标记(mark)+ 传播(propagate)+ 按需求值(compute on read)"的更新模型。

响应式图更新的关键机制:一是脏标记传递——状态变化沿图向下游传播"可能已过期"的信号,但具体"是否真的需要重算"推迟到读取时结合依赖实际值判断(可用版本号/检查位优化:只有值真正变化才继续传播,值未变则传播停止);二是求值顺序——读取时按拓扑序从依赖向上重算(先算依赖再算自己),保证结果一致;三是 effect 调度——effect 在微任务中批量执行一次(同轮多次状态变化合并),避免同步反复执行。设计取舍:纯惰性(读取才算)省计算但首次读取有延迟;急切传播(值变立即重算)响应快但可能算"没人读"的值;现代实现(Solid/Preact Signals)采用"惰性标记 + 读时计算 + 批处理 effect"的组合,兼顾响应性与计算量。

核心讲"标记-传播-按需重算"三步模型:set 只标记脏、读取才计算、结果缓存。再讲传播的停止条件(值未变不继续传播)与 effect 微任务批处理,最后提惰性 vs 急切取舍,即完整。

#
★★

18. Signal.subtle.Watcher 与 effect 的语义差异及调度时机(微任务批处理 vs 同步触发)

Signal.subtle.Watcher 与 Signal.effect 在语义上有什么差异?它们各自的调度时机(微任务批处理 vs 同步触发)如何设计?

  • Watcher:底层订阅者(可手动管理订阅),effect:高层自动清理副作用
  • 调度:effect 微任务批量执行 vs Watcher 的按需/同步通知
  • 适用场景:框架接入(Watcher)与业务副作用(effect)

Signal.effect 是面向应用层的副作用原语:自动收集依赖、自动清理(重复执行时先清旧订阅)、在微任务中批量调度(同一轮多个状态变化合并为一次执行,避免同步抖动),语义是"声明式地跟踪一段副作用与依赖的生命周期";Signal.subtle.Watcher 是面向框架/库作者的底层 API:它不自动管理订阅生命周期,而是让调用方显式创建 Watcher、指定要观察的信号集合、手动调用 getPending()/回调获得"发生变化的信号列表",调度时机也交由调用方控制(可同步轮询、可在特定时机批处理),是"更底层的订阅原语"。

差异本质:一是抽象层级——effect 是"用完即走"的声明式 API(框架负责生命周期),Watcher 是"受控接入点"(消费方负责一切),方便框架按自己的调度模型接入信号(如在渲染循环开始前统一处理待变更信号);二是调度时机——effect 固定走微任务批处理(保证多次写入只执行一次副作用、避免中间态),Watcher 的通知可由调用方决定在何时以何方式消费(例如 React 可在渲染提交阶段同步处理);三是能力边界——Watcher 暴露"哪些信号变了"的批量信息,适合驱动 diff/重建决策,effect 隐藏这些细节直接执行副作用。设计权衡:微任务批处理牺牲了一点实时性换来确定性与性能(合并写),同步触发换实时性但可能中间态执行;框架层通常"微任务批处理 + 渲染前手动 flush"结合,应用层无脑用 effect 即可。

从"声明式高层 vs 底层受控"讲语义差异,再讲调度差异(微任务批量 vs 调用方决定),最后落到适用场景(框架接入用 Watcher、业务副作用用 effect),即完整。

#
★★

19. Signals 提案中「equals」函数(去重/相等判定)的作用

TC39 Signals 提案中 Signal.State/Computed 的 equals 函数有什么作用?如何利用它优化更新?

  • equals 用于判定新旧值是否相等、决定是否通知依赖
  • 默认严格相等与自定义相等(如深比较、容差比较)的场景
  • 优化效果:过滤"值未变"的无效传播

equals 是信号在 set 时使用的相等判定函数:默认使用 Object.is/严格相等,用于决定"新值是否与旧值相等"。若判定相等,信号认为"值没有实际变化",就不会把依赖标记为脏、不触发 effect 与下游传播;若不等,则正常传播更新。它把"更新是否必要"的决策权交给开发者,是响应式系统过滤无效更新的第一道闸门。

典型用法:一是自定义相等语义——对数组/对象做"结构相等"判断(如 set 进相同内容的数组不触发刷新)、对数值做容差比较(如浮点坐标位移小于阈值不更新)、对不可变数据用引用相等(配合 Immer 的引用比较,produce 后引用未变则跳过);二是对 computed 的返回值做判定——computed 每次重算出的值若与上次"相等"(如相同字符串、相同对象引用),则下游 effect/组件不刷新,避免"算了但没变还要通知"的浪费;三是提升高频场景性能——实时数据流中大量"数值没变"的写入,equals 能直接掐断传播链,减少下游无效计算。注意:equals 只在"写"侧生效,读取不受影响;自定义 equals 要保证一致性(不能一会相等一会不等),且对复杂结构做深比较本身有开销,需权衡(值语义适合、大对象建议用引用/版本号)。

先讲 equals 的机制(set 时判定、相等则掐断传播),再讲三类用法(结构相等、容差、引用相等),最后补"写侧生效、深比较开销"的注意点,即完整。

#
★★

20. Signal.Computed 的缓存失效(dirty/checked)状态机

Signal.Computed 的缓存失效是如何用 dirty/checked 状态机实现的?各状态与传播规则是什么?

  • computed 的三个状态:clean/dirty/checking 的含义
  • 传播规则:依赖脏则标记 dirty,读取时按状态决定重算/检查/复用
  • 优化:值未变的传递停止与 check 阶段的递归验证

Computed 的状态机一般用三态:clean(缓存有效,直接返回缓存值)、dirty(依赖可能已变,需要重算)、checking(正在验证依赖,防止环与重复计算)。写入 State 时,沿依赖图把下游 computed 标记为 dirty(可能过期);读取 computed 时依据状态分流:clean → 直接返回缓存;dirty → 重算(先递归让依赖更新)并缓存;checking → 表示求值链上正在处理该节点(用于环检测)。这就是"惰性失效 + 读时重算"的状态机实现。

传播的优化规则:dirty 标记是"可能过期"而非"必然过期"——当依赖状态 set 后值未变(equals 判定相等)时,传播在"值未变"的节点处停止,下游保持 clean,这是"检查位(checked flag)"优化的基础;读取 dirty 节点时,若其依赖实际值未变(递归检查依赖的版本/值),则该 computed 也可保持旧缓存(无需重算),即"dirty 是机会性重算"。现代实现(如 Preact Signals、Solid)用版本号(global version)做 O(1) 判定:每个信号记录"最后更新版本",computed 记录"最后计算时的版本",读取时比较即可判断缓存是否过期,避免递归遍历。理解状态机的作用:它保证"同一次变更只触发必要的重算"——不重算没人读的、不重算值没变的、重算有依赖的实际变化,把惰性求值的正确性与性能统一起来。

先讲三态语义与"set 标脏、读时重算"的主流程,再讲"值未变停止传播"与"版本号 O(1) 判定"的优化,最后说明状态机保证"必要重算"的目标,即完整。

#
★★

21. SolidJS 的 createSignal/createStore 无虚拟 DOM 更新原理

SolidJS 的 createSignal/createStore 是如何在没有虚拟 DOM 的情况下完成更新的?其"编译期 + 细粒度"的原理是什么?

  • 编译期模板转译:JSX 中表达式编译为细粒度 effect 的更新函数
  • 运行时:信号依赖收集与 DOM 节点级精确更新
  • createStore 的代理路径更新与不可变语义

Solid 的更新链路分编译期与运行期:编译期,编译器把 JSX 中所有"插值表达式"(如

{count()}
)解析出来,为每个表达式生成独立的细粒度更新函数(effect 体),把模板编译为"创建 DOM + 注册更新闭包"的代码;运行期,createSignal 返回 [getter, setter],getter 在更新函数执行时被读取并被依赖收集(与当前 effect 建立订阅),setter 触发时框架只执行订阅了该信号的更新函数,直接修改对应 DOM 节点(textContent、属性、class),全程没有虚拟 DOM 生成与 diff——更新路径是"信号 → 订阅它的更新函数 → 对应 DOM 节点"。

createStore 是深层的不可变状态模型:基于 Proxy 实现路径代理——读写按路径(store.a.b)操作,更新时用不可变方式产生新对象(结构共享),但 set 以路径 API 暴露(setStore('list', i, 'name', v));依赖追踪在"表达式读取的路径"上进行(读取 store.a.b 只订阅该路径),路径级精确更新;配合 immutable 语义(返回新引用),天然支持不可变数据协作。无虚拟 DOM 的收益:无虚拟树对象与 diff 阶段,内存占用低、更新开销与"变化的表达式数量"成正比;代价:手动优化空间小(不能靠 memo 裁剪,因为本来就地更新)、依赖编译期转译(动态结构如 map 生成需要编译指令辅助,如 组件)、调试需熟悉信号模型。适用场景:高频更新、大列表、对内存敏感的界面。

答题分编译期(JSX 表达式转细粒度更新函数)与运行期(订阅 → 节点直达),再讲 createStore 的路径代理与不可变语义,最后补收益与代价,即可覆盖"无虚拟 DOM 更新原理"。

#
★★

22. Preact Signals 如何在 React 生态中实现细粒度更新

Preact Signals 在 React 中是如何实现细粒度更新的?它绕过了 React 的哪些机制?有什么限制?

  • @preact/signals-react 的机制:信号读写在 React 渲染与事件中的拦截
  • 绕过整组件重渲染:渲染期订阅 + 提交期精准更新
  • 限制:并发特性、版本兼容、混用边界

@preact/signals-react 通过在 React 内部"挂钩"实现细粒度:核心机制是对组件渲染函数中读取信号的地方做"自动订阅"——渲染期间读取信号会建立该组件对信号的依赖,信号变化时不是简单触发整组件重渲染,而是(在支持的版本上)利用自定义 reconciler/渲染钩子,把变化映射为对"读取位置对应的 DOM 节点"的精准更新;同时它对事件处理器中的信号写入做批处理(同一次事件里的多次写合并)。本质上是在 React 之上叠加了一层"信号驱动的 DOM 更新层",让高频值更新不经过 React 的组件级 render-diff 流程。

实现要点:一是渲染期收集——组件渲染时读取信号的表达式被标记,变化时仅重执行这些"信号读取点"(类似 useSignal 的 memo 化,把组件拆成细粒度更新单元);二是提交期刷新——信号变化先标记,在 React 提交/帧回调时统一应用 DOM 更新,避免与 React 调度冲突;三是兼容层——通过集成 React reconciler 或对特定 React 版本打补丁实现。限制:一是对 React 版本敏感(依赖 React 内部实现细节,某些大版本升级需适配);二是并发特性(startTransition、Suspense 的时间切片渲染)下,信号更新与 React 调度的交互需要谨慎处理,可能存在边界行为差异;三是只能覆盖"由信号驱动"的更新,React 自身状态(useState)仍走组件级路径,混用时要明确边界;四是生态约束——第三方库若按 React 模型优化(memo/selector),与信号直更的配合需要测试。适用场景:React 项目中的高频局部更新(图表、编辑器、实时面板)用信号层补充,其余保持 React 习惯。

先讲机制(渲染期订阅 + 提交期精准更新、事件批处理),再讲对 React 的"绕过"(不经组件级 render-diff),最后列限制(版本敏感、并发边界、混用规则),即完整。

#
★★

23. Angular Signals 与 Zone.js 解耦后的变更检测模型

Angular 引入 Signals 后如何与 Zone.js 解耦?解耦后的变更检测模型是怎样的?

  • Zone.js 的"猴子补丁 + 全局通知"变更检测模型及其问题
  • Angular Signals 的显式依赖追踪与按需触发变更检测
  • 解耦后的模型:signal 变化 → 标记组件 → 局部/按需检测

传统 Angular 依赖 Zone.js 做变更检测:Zone.js 通过猴子补丁拦截异步 API(setTimeout、Promise、事件、XHR 等),任何异步操作后通知 Angular"可能有状态变化",Angular 默认对整个组件树从上到下跑一遍变更检测(检查所有绑定表达式),模型是"全局扫描 + 无差别检测"——问题在于异步操作与状态变化没有关联性(任何异步都会触发全树检测)、难以精确知道"哪个组件的数据变了"、以及 zone 补丁带来的性能与兼容成本。

Angular Signals 引入后提供"显式响应式"路径:状态用 signal() 创建、派生用 computed()、模板或 effect 中读取信号时建立依赖关系(Angular 编译模板时记录每个绑定读取了哪些信号),信号变化时框架精确知道"哪些模板绑定/组件依赖它",只对相关组件执行变更检测(onPush 语义天然化),不再需要 Zone.js 的全局通知——这就是"zoneless"(无 zone)模式:变化来源明确(信号 set)、影响范围精确(依赖图)、检测局部化(只检相关组件)。模型总结:信号写入 → 沿依赖图传播 → 标记受影响组件 → 在变更检测周期(微任务调度)中只检测被标记组件与相关绑定;检测期间对比信号值而非表达式重估全部。价值:性能可预测(更新成本与变化量相关)、摆脱 zone 补丁(更小运行时、SSR/微前端兼容更好)、诊断更清晰。注意事项:zoneless 要求全链路用信号或显式触发(markForCheck/ChangeDetectorRef),混用 zone 风格(未用信号的状态 + 异步回调)时需明确检测触发方式。

先讲 Zone.js 模型的机制与问题(全局扫描、无关联性),再讲 Signals 模型的机制(模板依赖收集 + 按需检测 + 局部标记),最后对比价值与混用注意,即覆盖"解耦后的变更检测模型"。

#

24. 为什么 Signals 有望成为跨框架统一的状态原语

为什么 Signals 有望成为跨框架统一的状态原语?统一的收益与可能障碍是什么?

  • 状态管理需求的共性:依赖追踪、派生、副作用,与渲染解耦
  • 各框架已收敛到信号模型(Solid/Angular/Preact/Vue 借鉴)
  • 统一收益(互操作、工具、心智)与障碍(框架差异化、标准落地)

统一的基础是"状态原语"的通用性:任何前端应用都需要"存储值 + 派生计算 + 副作用响应",这套需求与具体渲染模型(虚拟 DOM、细粒度、编译优化)无关,Signal 正是该需求的"最小通用模型"(State/Computed/effect),各框架可以各自消费它而不必重复发明依赖追踪;同时行业已出现事实收敛——Solid 原生信号、Angular 全面采用、Preact 信号跨 React 使用、Vue 响应式与 Vapor 向细粒度靠拢,说明"信号范式"是社区共识方向,TC39 提案提供了标准化的锚点(阶段推进中)。

收益:一是互操作——跨框架组件、普通 JS 模块、库之间能以统一信号交换状态,第三方库一次实现处处可用;二是工具链共享——Devtools、调试器、序列化、时间旅行、测试工具基于标准信号语义实现一次;三是心智收敛——开发者学习一次信号语义即可迁移各框架,招聘与团队流转成本降低;四是底层演进共享——调度、内存优化等改进惠及所有框架。障碍:一是框架差异化诉求——各框架的信号 API 与渲染深度绑定(Vue 的深代理、Solid 的访问器、Angular 的输入/effect),统一接口可能限制框架特色与极致优化(如编译期依赖分析);二是标准落地节奏——TC39 提案从 Stage 3 到各引擎实现、再到生态采用需要数年,期间各框架自研实现是常态,出现"标准语义 vs 框架方言"并存期;三是生态惰性——存量代码与工具绑定现状,迁移到标准信号的动力需要收益足够显著。

从"需求共性 + 行业收敛 + 标准锚点"讲有望统一,再分别讲统一收益(互操作、工具、心智、演进)与障碍(框架特色、落地节奏、生态惰性),最后落到"标准底层 + 框架差异共存"的现实预期,即完整。

#

25. 从 Redux/MobX 迁移到 Signals 心智模型的转变

从 Redux/MobX 迁移到 Signals 心智模型需要哪些转变?迁移时容易踩哪些坑?

  • Redux:单一 store + action/reducer 的显式数据流 vs 信号的分散局部状态
  • MobX 的自动追踪与 Signals 的显式订阅差异
  • 迁移坑:派生重复计算、订阅生命周期、工具链差异

从 Redux 迁移:心智从"全局单 store + action/reducer + selector 推导"转为"局部信号 + 组合派生 + 直接读取"——Redux 强调集中式显式数据流(所有变更走 action、reducer 纯函数、状态提升),Signals 鼓励状态就近放置(组件/模块内信号)、跨模块用 signal 引用或 context 传递;派生从 selector 的 memo 变为 computed 的惰性缓存,功能等价但书写位置从"选择器文件"移到"数据定义处";异步从 thunk/saga 的中间件模型变为"async computed/effect 直接驱动"(配合 resource 原语)。从 MobX 迁移:MobX 是"自动追踪的可变响应式"(观察对象、自动推导依赖),Signals 是"显式值容器"——访问方式(.value vs 属性读)与可变性边界(MobX 深层可变、Signal 值替换)需要调整;MobX 的 action 约束在 Signals 中靠"写入者唯一"与 readonly 约束实现。

常见坑:一是"把 Redux 的全局 store 原样搬成全局信号"——丧失局部化收益,应重新按模块/页面划分状态边界;二是派生重算的隐式差异——Redux 的 selector 在每次 dispatch 后重估(靠 memo 裁剪),Signals 的 computed 惰性按需重算,若把"副作用"写进 computed(Redux 里 selector 可容忍、信号里是反模式)会产生 bug;三是订阅生命周期——信号 effect/订阅不随组件自动清理(React 中需 hook 封装),泄漏导致内存问题与重复执行;四是工具链差异——Redux DevTools 的时间旅行/中间件能力需要替代方案(信号的调试工具、日志中间件需自行实现);五是同步节奏——Redux 的 dispatch 同步 + 订阅批量,与信号的微任务批处理在时序上有差异,依赖"同步拿到最新值"的代码需改用 effect。迁移建议:渐进式——新状态用信号、边界处桥接(信号 → Redux dispatch 或反向),而不是一次性重写。

分别对比 Redux(集中显式 vs 局部组合)与 MobX(自动可变 vs 显式值容器)的迁移点,再重点列五个坑(全局化误用、computed 副作用、生命周期、工具链、时序),并给渐进迁移建议,即完整。

#

26. 细粒度响应式对高频更新(实时数据/协作编辑)的收益量化

细粒度响应式对高频更新场景(实时数据、协作编辑)的收益如何量化?收益的边界在哪里?

  • 量化指标:单次更新耗时、FPS、CPU/内存、更新节点数
  • 高频场景的更新模式(大量小值变更、局部刷新)与细粒度的匹配
  • 收益边界:批量写、结构变化、渲染预算等其他瓶颈

量化细粒度收益需要对照"基线":同一场景分别用组件级重渲染(React 默认)与细粒度(Signals/Solid)实现,对比关键指标——单次更新耗时(P50/P95)、持续更新下的帧率(FPS 与掉帧率)、CPU 占用、内存曲线、以及"每次更新实际触及的节点数 vs 组件数"。实时数据场景(行情、监控大屏、协作光标)的典型模式是"大量小值高频变更"(每秒几十~几百次、每次只改几个字段),此时组件级方案每次都要跑整组件 render-diff(成本 ∝ 组件规模 × 频率),细粒度方案只执行绑定节点的更新(成本 ∝ 变化值数量 × 频率),量化上体现为:节点数万级时组件级更新耗时随组件数线性增长而细粒度基本持平,实测常见 2~10 倍吞吐差距;协作编辑(多人光标、在线字段)还叠加"局部批量"特征,细粒度 + 批处理组合收益更大。

收益边界:一是批量写场景——同一帧内大量状态同时变更(如 store 整体替换),细粒度与组件级都需要一轮批处理,差距缩小;二是结构级变化——列表增删、子树替换仍是结构协调成本,细粒度不改变该开销(仍需要虚拟化/结构 diff);三是渲染预算——DOM 节点总量、布局与绘制(重排/重绘)是最终瓶颈,细粒度只优化"JS 更新调度"环节,节点多到布局吃不消时收益受限;四是框架外成本——网络、数据解析、Canvas 绘制本身不因响应式粒度改变。量化实践:用基准套件(如 js-framework-benchmark 的子场景)记录上述指标,以"目标帧率 × 更新频率"核算场景预算,选择粒度与优化手段(虚拟化、脏区重绘、worker 计算)。

先给量化方法(对照基线、指标集、更新模式匹配),再给量化结论(成本随变化量 vs 随规模),最后明确边界(批量写、结构变化、渲染预算),即构成完整量化分析。

#

27. 响应式调试工具(如 Solid Devtools)追踪依赖关系

响应式调试工具(如 Solid Devtools、Vue Devtools)如何追踪依赖关系?利用这些工具能排查哪些问题?

  • 工具原理:运行时挂载/包装信号,记录依赖图与更新事件
  • 可排查问题:无效依赖、过度重算、更新泄漏、渲染根因
  • 实践用法:依赖列表、触发溯源、性能分析

响应式调试工具通过"运行时注入"工作:Devtools 插件把框架的信号/effect 创建过程包装(hijack),在信号读取时记录"谁读了我"、写入时记录"更新了谁",从而在开发者工具中呈现依赖图——每个信号的订阅者列表、每个 computed/effect 的依赖列表、以及更新事件的触发链(某次 set 最终触发了哪些更新)。Solid Devtools 提供信号树视图、组件与信号的映射、以及每次更新的耗时与调用栈;Vue Devtools 的响应式追踪面板(effect/依赖视图)与 Angular Devtools 的信号检查器同理,都是"依赖图可视化 + 更新溯源"。

能排查的问题:一是无效依赖/过度订阅——查看某 effect 的依赖列表,发现与逻辑无关的信号(如多余读取),删除后消除伪更新;二是过度重算——通过更新计数与触发源定位"经常被无谓触发的 computed",结合 equals 与粒度拆分修复;三是更新泄漏——检查已卸载组件是否仍持有订阅/信号(effect 未清理),工具中表现为"僵尸更新"事件;四是渲染根因——一次界面变化到底由哪个状态、哪条链路引起,回放触发链(signal → computed → effect → DOM 更新)快速定位非预期更新的来源;五是性能瓶颈——对比各更新函数的耗时,找出热点(如大型 computed、高频 effect)。实践建议:开发期常开依赖图面板、为关键状态命名(信号命名规范便于识别)、对可疑更新用"触发溯源"断点定位,并在 CI/评审中结合调试结果沉淀依赖设计规范。

先讲工具原理(运行时包装 + 依赖图记录),再列四类可排查问题(无效依赖、过度重算、泄漏、渲染根因),最后给实践用法,即可覆盖"追踪依赖关系"考点。

#

28. 跨框架的 Signal 标准提案(TC39)现状与生态影响?

TC39 Signals 提案的现状如何?它落地后对前端生态会带来哪些影响?

  • 提案现状:Stage 1、核心 API 与 subtle 底层接口、浏览器支持
  • 对框架层(内核、互操作)与库生态(状态库、工具链)的影响
  • 对开发者的影响与演进预期

TC39 Signals 提案(ECMAScript proposal-signals)由各框架(Solid、Angular、Preact、Vue 等)与浏览器厂商合作推动,目前处于 Stage 1,核心 API 为 Signal.State/Computed/effect(应用层)与 Signal.subtle(Watcher 等底层接口,供框架/库接入),配套社区库(signal-polyfill、signal-utils)提供垫片与工具;处于 Stage 1 意味着提案仍处早期阶段、语义与 API 仍在演进,接下来是规范的逐步成熟与各引擎(V8/JSC/SpiderMonkey)的参考实现、性能打磨,浏览器原生支持尚需时间,现阶段通过 polyfill 使用。

生态影响分几层:框架层——各框架可把响应式内核逐步对齐标准信号(内部实现替换或桥接),减少自研依赖追踪的维护成本,跨框架共享状态成为可能,同时催生"标准信号 + 框架方言"的分层格局;库生态——状态管理库(Redux/MobX 的替代、signal 工具库)、跨框架组件库、数据可视化库可基于统一信号做互操作,"一次实现、处处可用"降低生态碎片化;工具链——Devtools、调试、序列化、测试工具围绕标准语义统一实现,提升诊断能力;浏览器/平台——引擎原生支持后无 polyfill 成本、与 Web 平台能力(如标准化的调度)结合,性能进一步优化。对开发者:信号心智成为"前端公共知识"(类似 Promise),跨框架迁移成本下降;同时要适应"标准信号与框架自有 API 并存"的过渡期,理解何时用原生信号(跨框架共享、非框架代码)何时用框架 API(组件内)。预期:提案后续推进 Runtime Hooks、异步信号等扩展,生态影响是"渐进式渗透"而非一夜替换。

先讲现状(Stage 1、API 组成、polyfill 阶段),再分层讲影响(框架内核、库生态、工具链、开发者心智),最后给演进预期(渐进渗透、双层并存),即完整覆盖"现状与影响"。