响应式编程:推、拉与背压
推和拉可以先用一个问题区分:谁决定什么时候传递数据?这篇是 2025 年整理响应式编程时的概念笔记,主要看数据源与消费者之间的关系。
响应式编程关注数据流和变化传播。数据发生变化后,依赖它的计算或界面也随之更新。异步事件是常见的使用场景,不过这里先把“何时产生数据”和“何时消费数据”拆开来看。
推:数据源主动通知
推模型中,数据源产生变化后通知消费者。Observable / Publisher 与 Observer / Subscriber 是响应式库里常见的命名。
例如新闻源产生一条新闻,可以直接把内容发给订阅者,也可以只发出“新闻更新了”的信号。订阅者收到后再执行相应逻辑,如更新界面。这和观察者模式的关系比较直观:被观察对象发生变化,通知观察者。
推模型适合聊天消息、股票行情、传感器数据,以及按钮点击、鼠标移动、键盘输入、文件系统变化等事件。消费者不用反复询问有没有新数据。
问题在于节奏。如果数据产生得比处理得快,消费者可能积压任务。单纯的推送机制不会自动解决这个问题,需要另外安排缓冲、丢弃或流量控制。也不能只凭“推”就判断实现一定实时或高效,通知之后仍有处理成本。
拉:消费者主动请求
拉模型中,消费者需要数据时才向数据源请求。迭代器的 next() 是一个容易理解的例子:请求下一个元素,处理完,再决定是否继续。
一次拉取可以返回数据项,也可以返回结束信号。消费者因此能够按自身处理能力安排请求频率。
读取大文件、逐项遍历列表、批量查询或在资源有限的设备上处理数据,都适合用这种方式思考。分块读取也能避免一次性把大量数据放进内存。
拉取让消费节奏更可控,但不代表上游一定没有积压。原笔记把它概括为“天然支持背压”,这里需要保留一个前提:数据源能否按请求生产、暂停或保存数据,要看具体实现。拉模型也不等同于慢,是否及时取决于请求和处理方式。
推拉结合:通知之外,还要管需求量
混合模型把数据通知和消费需求联系起来。消费者告诉数据源自己还需要多少数据,数据源按这个需求量发送;处理能力不足时,消费者减少请求或暂停消费。
RxJava、Reactor 和 Vue 响应式系统都可以用来讨论变化通知,但把它们统一归入“推拉结合”有局限:不同库里的推拉机制不能直接视为同一种流量控制协议。Vue 的依赖通知与取值,也不宜直接套用下面的数据流背压策略。
背压(Backpressure)关注的是生产速度与消费速度不匹配时怎么办。这里整理几种处理方式:
- 丢弃(Drop):处理不过来时丢弃数据。代价是数据可能丢失,需要先判断业务能否接受。
- 缓冲(Buffer):将数据放进队列,等待处理。无界缓冲可能持续占用内存;有界缓冲则需要规定队列满时的行为。
- 请求(Request):消费者明确请求一定数量的数据,数据源按请求量发送。
- 响应式拉取(Reactive Pull):根据消费能力调整请求节奏。这里用这个名称描述动态调整,具体术语和机制仍要按所用库理解。
request()、onBackpressureBuffer()、onBackpressureDrop() 是当时记录的 API 名称,用来对应请求、缓冲和丢弃。它们属于具体库的机制,不能当成所有响应式库都有的通用操作符。
放在一起比较
| 模型 | 谁发起传递 | 消费节奏 | 需要留意的问题 | 原笔记中的场景 |
|---|---|---|---|---|
| 推(Push) | 数据源 | 跟随数据产生与通知 | 消费者是否处理得过来 | 实时消息、UI 事件、系统事件 |
| 拉(Pull) | 消费者 | 按需要请求 | 上游是否能按需供给 | 文件读取、有限集合、批量处理 |
| 推拉结合 | 通知与需求量共同参与 | 依具体机制协调 | 请求、缓冲、丢弃的契约 | 需要控制消费速度的数据流 |
前端里先看具体操作
前端经常处理用户输入、网络请求和状态变化。这时我更关心几个实际问题:事件过密时是否需要减少处理次数?新请求到来时,旧请求的结果还要不要?上一项没处理完,下一项应该排队还是忽略?
debounceTime、switchMap、exhaustMap 分别涉及事件频率与并发请求的处理。它们有各自的语义,选择时应看业务需要,不能统一称为背压。
原文还用 RxJS 的 Subject 和 from 对比推拉。这个说法容易把“输入可以迭代”与“订阅后的通知方式”混在一起,因此这里只保留为待核对的阅读问题,不据此选择操作符。