Push-based vs. Pull-based 响应式:细粒度系统背后的两大驱动模型
Source: Dev.to

回顾
基于之前关于响应性的核心思想的文章,这部分阐明 Push‑based 与 Pull‑based 响应式模型之间的区别。
核心思想
在细粒度响应式系统中:
- Push‑based(推送式)系统在值变化时立即执行计算。
- Pull‑based(拉取式)系统会延迟计算,直到有人读取该值时才进行。
实际案例
Push‑based
想象在美食广场点餐。
- 写入(下单): 你下单。
- 推送(完成通知): 当餐点准备好时,你的呼叫器会震动或亮灯——更新被直接推送给你。
- 效果(取餐): 你走到柜台去取餐。
响应式解释: 当源发生变化时,依赖节点会立即重新计算并立刻得到通知。
Pull‑based
现在想象去买一杯奶茶。
- 写入(下单): 你下单点饮料。
- 标记(仅更新状态): 当饮料准备好时,店家只会在屏幕上显示你的号码——他们不会直接通知你。
- 读取 → 计算(仅在需要时): 当你查看屏幕时,这个读取操作会触发“哦,已经好了,我该去取了”。
- 效果(取餐): 你去柜台。
响应式解释: 写入仅标记节点为脏;真正的计算在有人读取该值时才进行。
正式定义
| 模型 | 行为焦点 | 简化流程 |
|---|---|---|
| Push‑based | 写入时计算:更新立即传播 | set() → propagate → compute → effect |
| Pull‑based | 写入时标记,读取时计算 | set() → markDirty ⏸ read() → if dirty → compute → effect |
关键洞察: 两种模型都“推送”信号——Push 推送计算,而 Pull 推送脏标记。
时间线图示
Push‑based
Pull‑based
优缺点
| 方面 | Push‑based | Pull‑based |
|---|---|---|
| 读取延迟 | 最低——始终最新 | 第一次读取可能触发重新计算 |
| 写入成本 | 可能较高:O(depth × writes) | 较低:大多为 O(depth × 1)(仅标记脏) |
| 过度计算 | 高——即使从未读取也会计算 | 低——仅在实际读取时计算 |
| 批处理 | 困难——工作已经完成 | 自然适配——稍后一次性刷新 |
| 调试可见性 | 依赖链立即展开 | 需要 DevTools 检查拉取发生时机 |
| 最佳使用场景 | 高频写入、低读取(例如协作应用中的光标同步) | 低写入、高读取(仪表盘、图表) |
注: Pull‑based 系统在标记阶段仍会遍历依赖图,但它们不重新计算——仅将 dirty = true 设置为脏。
如何选择?
| 用例 | 推荐模型 | 原因 |
|---|---|---|
| 实时协作、游戏状态同步 | Push | 立即反映比避免额外计算更重要 |
| 大型仪表盘、数据可视化 | Pull | 写入稀少,读取频繁——仅计算实际需要的内容 |
| 时间线/滚动驱动动画 | Pull + Scheduler | Pull 推迟工作;调度器确保每帧最多一次重新计算 |
| 数据管道(昂贵计算多次复用) | Push‑on‑Commit | 预先一次性计算,然后在各处复用 |
React 基本上是 Pull + Scheduler,这就是批处理工作方式的原因。
RxJS 和 MobX 是 Push‑on‑Commit 的经典例子。
常见误解
- “Pull 意味着扫描整个图!”
不——Pull 只在读取值时检查向上的相关依赖链。无需遍历

