响应式的演进:UI 更新学会自行处理

发布: (2025年12月8日 GMT+8 09:54)
7 min read
原文: Dev.to

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 频谱上,就能得到一条清晰的响应式演进时间线。

pull push timeline

汇总表

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

模型细节

脏检查

Dirty checking flow

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

虚拟 DOM Diff

vdom flow

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

Watcher / Observable Graph

observer flow

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

细粒度 Signals

signal flow

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 保证可预测性

结论

响应式的历史本质上是 PushPull 之间的平衡史。

  • 我们从纯 Pull 的脏检查时代起步。
  • 随后出现了混合模型,如虚拟 DOM。
  • 再后来,基于依赖图的系统实现了细粒度的 Push + Pull 混合。

运行时 Signals 往往采用 push 失效 + pull(惰性)评估,而编译时 Signals 将工作前移到构建阶段,逼近 纯 Push 响应式。

到此,你应该已经清晰了解每种模型在以下方面的差异:

  • 谁发起更新?
  • 更新的传播范围有多大?

但仍有一个关键问题未得到解答:

即使在细粒度

Back to Blog

相关文章

阅读更多 »