Skip to content

第 3 章 响应式系统设计哲学

本章要点

  • 响应式编程的本质:从命令式手动同步到声明式自动传播
  • Vue 响应式的三代实现:defineProperty → Proxy → Alien Signals
  • 细粒度 vs 粗粒度响应式:Vue 与 React 的根本分歧
  • 与 MobX、Solid Signals、Svelte Runes 的横向对比
  • "精确传播变化"——响应式系统的终极追求

假设你在管理一家咖啡店的库存。每天早上,你需要做三件事:

  1. 检查咖啡豆存量
  2. 根据存量计算今天能做多少杯咖啡
  3. 如果不够,给供应商打电话补货

用命令式编程的方式,代码大概是这样的:

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 变化时,cupsAvailableneedRestock 不会自动更新。你必须手动重新计算。在这个简单例子中,手动同步还能应付。但在一个有数百个相互依赖的状态的前端应用中,手动同步就是噩梦的起点。

这就是响应式系统要解决的核心问题:让数据之间的依赖关系自动维护。

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)。图中的节点分为三种:

  1. 信号(Signal):源数据,如 ref(0),是依赖图的叶节点
  2. 计算(Computed):从信号或其他计算派生的数据,如 computed(() => count.value * 2)
  3. 副作用(Effect):当依赖变化时需要执行的操作,如 DOM 更新、日志打印

price 变化时,系统需要:

  1. 重算 total(因为它依赖 price
  2. 重算 tax(因为它依赖 total
  3. 重新执行两个 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、RxJSAlien Signals、Solid.js

基于 VitePress 构建