响应式的演进:UI 更新学会自行处理
Source: Dev.to
简要历史
早在 2010 年,Knockout.js 将 Observable(可观察对象)和 Computed(计算属性)的概念引入前端世界。
这是浏览器首次拥有一种实用方式,让数据先发声——UI 自动跟随。
从那一刻起,每个主流框架背后的核心争论变成了:
框架应该主动“检查”数据,还是数据主动“通知”框架?
回顾过去,Knockout 并没有赢得人气赛,但它种下的概念种子最终塑造了三大框架:Angular、React 和 Vue。自动响应式的想法改变了一切。
本文聚焦于 UI 响应式(与 FRP 有关,但并不完全相同),以及它的演进如何影响 Angular、React、Vue,最终催生了现代 Signals 运动。
响应式到底意味着什么
响应式把 UI 更新从:
❌ “数据变化时手动修改 DOM”
转变为
✅ “描述 UI 应该是什么样子——系统负责其余工作。”
核心原则
声明式
你描述 UI 应该是什么样子,系统决定如何把更新渲染到屏幕。
依赖追踪
程序首次读取某个值时,系统会悄悄记录“谁依赖了什么”。
变更传播
当数据变化时,系统向所有依赖者发送失效信号,只更新必要的部分。
为什么重要?
- 降低认知负担:不再需要担心“我是否记得更新 X?”
- 性能提升:只更新实际改变的部分——不再整页重绘。
- 更清晰的数据流:更易调试,行为可预测。
两大核心策略:谁先说话?
| 策略 | 典型实现 | 关键字 |
|---|---|---|
| Pull(框架请求数据) | 循环、diff | 脏检查、VDOM diff |
| Push(数据通知框架) | 观察者、信号 | observable、effect |
大多数现代框架实际上是混合型:数据推送失效 → 框架在恰当时机拉取计算或 diff。
四种响应式模型
如果把主要方法放在 Pull ↔ Push 频谱上,就能得到一条清晰的响应式演进时间线。

汇总表
| 模型 | 更新流向 | Push/Pull 位置 | 粒度 | 代表框架 |
|---|---|---|---|---|
| 脏检查 | $digest 扫描所有 $watch → 同步更新 | 纯 Pull | 按表达式;观察者越多性能越差 | AngularJS 1.x |
| 虚拟 DOM diff | setState 推送脏标记 → 批量重渲染 → VDOM diff → DOM 打补丁 | 混合 | 组件子树;模型简单,但可能过度渲染 | React、Preact、Vue 2 |
| Watcher / Observable Graph | setter 推送 → 观察者仅重新计算其子树 | 偏 Push | 基于 getter 的依赖追踪;粒度更细 | Vue 2 Watchers、MobX |
| 细粒度 Signals | setter 推送 → 值惰性拉取重新计算 → 直接更新 DOM | 运行时混合或编译时接近纯 Push | 属性级或 DOM 节点级精度;无 VDOM | Solid.js、Angular Signals、Svelte 5 Runes |
模型细节
脏检查

纯 Pull 模型。框架反复扫描每个被观察的表达式以判断是否有变化。可预测但代价高——性能随观察者数量线性增长。
虚拟 DOM Diff

React 引入批处理、失效标记和 diff 阶段。Push 步骤将组件标记为脏,Pull 步骤通过 diff 解析新 UI。开发体验佳,但有时会执行不必要的工作。
Watcher / Observable Graph

依赖通过 getter 建立。只有依赖于被修改值的观察者会重新运行,显著减少不必要的重新计算。
细粒度 Signals

Signals 不再处理庞大的组件树,而是作用于最小的响应式单元:
- 值
- memo
- 甚至原始 DOM 节点
运行时 Signals(Solid、Angular Signals)使用 push 失效 + pull(惰性)重新计算。
编译时 Signals(Svelte 5)把大部分工作推到编译器,接近纯 Push 模型。
关键对比
1. Push vs Pull — 谁发起工作?
- 脏检查:纯 Pull
- 虚拟 DOM:push(脏)→ pull(diff)
- Watcher / Proxy / Signals:push 失效 → pull 评估(惰性)
- 编译时 Signals:依赖在编译期确定,接近纯 Push
2. 依赖精度 — 系统有多精准?
- 虚拟 DOM:知道“哪个组件子树可能需要更新”。
- 运行时 Signals / Proxy:知道“到底是哪一个 memo 或 DOM 节点变化”。
- 编译时 Signals:直接输出最终的 DOM 操作——精度最高。
3. 调度 — 更新何时真正执行?
- React / Solid:通过 microtask 批处理;把多次写入合并到同一轮 tick
- Vue 3:作业队列
- 脏检查:同步循环;成本随观察者线性增长
4. 心智模型 — 编码感受如何?
- Signals / MobX:感觉像“直接改值”——系统负责传播
- 虚拟 DOM:接受“render ≠ paint”的声明式工作流
- 编译时 Signals:更接近原生 JavaScript;编译器 + IDE 保证可预测性
结论
响应式的历史本质上是 Push 与 Pull 之间的平衡史。
- 我们从纯 Pull 的脏检查时代起步。
- 随后出现了混合模型,如虚拟 DOM。
- 再后来,基于依赖图的系统实现了细粒度的 Push + Pull 混合。
运行时 Signals 往往采用 push 失效 + pull(惰性)评估,而编译时 Signals 将工作前移到构建阶段,逼近 纯 Push 响应式。
到此,你应该已经清晰了解每种模型在以下方面的差异:
- 谁发起更新?
- 更新的传播范围有多大?
但仍有一个关键问题未得到解答:
即使在细粒度