Appearance
第 3 章 响应式系统设计哲学
本章要点
- 响应式编程的本质:从命令式手动同步到声明式自动传播
- Vue 响应式的三代实现:defineProperty → Proxy → Alien Signals
- 细粒度 vs 粗粒度响应式:Vue 与 React 的根本分歧
- 与 MobX、Solid Signals、Svelte Runes 的横向对比
- "精确传播变化"——响应式系统的终极追求
假设你在管理一家咖啡店的库存。每天早上,你需要做三件事:
- 检查咖啡豆存量
- 根据存量计算今天能做多少杯咖啡
- 如果不够,给供应商打电话补货
用命令式编程的方式,代码大概是这样的:
typescript
let beans = 500 // 克
let cupsAvailable = Math.floor(beans / 15)
let needRestock = cupsAvailable < 20
// 第二天早上,有人用掉了一些豆子
beans = 200
// 糟了!cupsAvailable 和 needRestock 没有自动更新
console.log(cupsAvailable) // 仍然是 33,实际应该是 13
console.log(needRestock) // 仍然是 false,实际应该是 true
// 你必须手动重新计算
cupsAvailable = Math.floor(beans / 15)
needRestock = cupsAvailable < 20看到问题了吗?当 beans 变化时,cupsAvailable 和 needRestock 不会自动更新。你必须手动重新计算。在这个简单例子中,手动同步还能应付。但在一个有数百个相互依赖的状态的前端应用中,手动同步就是噩梦的起点。
这就是响应式系统要解决的核心问题:让数据之间的依赖关系自动维护。
typescript
import { ref, computed } from 'vue'
const beans = ref(500)
const cupsAvailable = computed(() => Math.floor(beans.value / 15))
const needRestock = computed(() => cupsAvailable.value < 20)
console.log(cupsAvailable.value) // 33
console.log(needRestock.value) // false
beans.value = 200 // 修改源数据
console.log(cupsAvailable.value) // 13 — 自动更新了!
console.log(needRestock.value) // true — 自动更新了!没有手动重新计算,没有 setState,没有 dispatch。数据变了,所有依赖它的计算自动保持一致。
这不是魔法。这是一套精心设计的因果传播系统。
3.1 响应式编程的本质:数据驱动的依赖图
什么是依赖图
响应式系统的核心数据结构是一个有向无环图(DAG, Directed Acyclic Graph)。图中的节点分为三种:
- 信号(Signal):源数据,如
ref(0),是依赖图的叶节点 - 计算(Computed):从信号或其他计算派生的数据,如
computed(() => count.value * 2) - 副作用(Effect):当依赖变化时需要执行的操作,如 DOM 更新、日志打印
当 price 变化时,系统需要:
- 重算
total(因为它依赖price) - 重算
tax(因为它依赖total) - 重新执行两个 effect(DOM 更新)
关键约束是:不能多做(重算不需要重算的),也不能少做(遗漏需要重算的)。 这就是"精确传播"的含义。
推模型 vs 拉模型
依赖图中变化的传播方式有两种基本策略:
推模型(Push):当信号变化时,立即沿依赖图向下推送通知。
price 变化 → 推送给 total → total 重算 → 推送给 tax → tax 重算 → 推送给 effects拉模型(Pull):当信号变化时,只标记为"脏"。下游节点在被读取时才检查上游是否脏,按需重算。
price 变化 → 标记 price 脏
...(什么都不发生,直到有人读取 total 或 tax)
读取 tax → 检查 total 是否脏 → 检查 price 是否脏 → 是 → 重算 total → 重算 tax → 返回新值| 维度 | 推模型 | 拉模型 |
|---|---|---|
| 触发时机 | 数据变化时 | 数据被读取时 |
| 无用计算 | 可能(推送给无人读取的节点) | 无(只计算被读取的节点) |
| 延迟 | 低(立即推送) | 可能更高(读取时才计算) |
| 适用场景 | 实时性要求高 | 计算密集但读取稀少 |
| 典型实现 | Vue 3.0–3.4、RxJS | Alien Signals、Solid.js |