RxJS · 从 Observable 标准契约,到它解决的那些问题
RxJS 常被当成「一大堆 operator」来记,但它的内核其实只有一个标准契约:Observable —— 一个可以脱离库、用十几行就手写出来的接口。subscribe(observer) 触发一个 producer,producer 通过 observer.next / error / complete 把值推给你,并返回一个 teardown 作为 Subscription 供取消。这正是 TC39 Observable proposal、以及如今 WHATWG DOM Observable (el.when('click')) 的形状。
本系列从这个契约本身出发: 先把它放进 singular/plural × pull/push 的标准坐标系里,看清它和 function / Iterator / Promise 的分工; 再依次拆 创建 → 变换 (operator) → 组合 (高阶 flatten) → 共享 (hot/cold); 最后落到工程里真正要解决的问题 —— 取消陈旧请求、防抖 autocomplete、拖拽、重试。每页都用 marble diagram 单步观察流随时间的演化。
契约:十几行手写一个 Observable
不引入任何库,从零手写 Observable:subscribe(observer) 调用 producer,producer 通过 observer.next / error / complete 推值,返回的 teardown 就是 Subscription。单步执行看值如何被推给 observer,并用 marble 画出这条流。对照 Promise 点出三处本质差异:lazy(订阅才执行)、多值 (next*)、可取消(unsubscribe 触发 teardown),以及语法契约 next* (error | complete)?。
坐标系:function / Iterator / Promise / Observable
把四种取值方式放进 singular / plural×pull / push 的 2×2 网格:单值+拉 = function,多值+拉 = Iterator,单值+推 = Promise,多值+推 = Observable。点每一格,右侧用真实代码演示各自的取值接口 (fn() / it.next() / p.then / obs.subscribe)。看清 Observable 补上的正是右下角那格——它是 Promise 在「多值」维度上的自然推广。
创建:把任何东西变成一条流
marble 引擎首次登场:选 of / from(iterable 或 promise)/ interval / timer / fromEvent,看它在时间轴上如何吐出 marble。重点是 cold + lazy——Observable 只是「配方」,不订阅什么都不会发生;每次 subscribe 都把 producer 从头重新执行一遍(页面用订阅计数演示),这与 hot 流的共享执行相对。
变换:pipe 把 operator 串成管道
上游 marble → operator → 下游 marble。单步推进时间游标,逐个看 map / filter / scan / take / takeWhile 如何逐值变换。核心观念:operator 是纯函数,吃一个 Observable 返回一个新的 Observable,不改上游;pipe(a, b, c) 只是函数组合,数据从上到下流过每一节。
高阶:一个值映射成一条流,四种拍平策略
当 map 的结果本身是 Observable,就得到高阶 Observable,需要 flatten 成一条流。同一条外层输入,四种策略并排对比:mergeMap 全部并发合并、concatMap 排队串接、switchMap 新的一来就取消旧的、exhaustMap 忙时忽略新的。这页是理解「消除请求竞态」的关键——autocomplete 为何用 switchMap,答案就在这四条 marble 的差异里。
冷与热:独立重放,还是共享一次执行
cold Observable 每个订阅者各触发一次独立执行(两条互不相干的 marble);hot 流(如 Subject)多个订阅者共享同一次执行,晚订阅者会错过之前的值。用两个订阅者并排演示差异,并说明 share / multicast 如何把一条 cold 流转成 hot——避免同一份网络请求被每个订阅者重复触发。
落地:这些问题,用流怎么优雅解决
把前面的契约与 operator 对上真实痛点,每个做成可交互 lab: autocomplete(debounceTime 合并抖动 + distinctUntilChanged + switchMap 丢弃陈旧响应,当场看竞态被消掉)、拖拽(mousedown → switchMap(mousemove 直到 mouseup),用 takeUntil 收尾)、retry / backoff(失败后延迟重订阅)。末尾一张「问题 → operator」速查表。
一句话串起来
Observable —— 推送式、惰性、可取消的多值序列 (见 契约),它在 pull/push × 单值/多值 坐标系里正好补上 Promise 缺的那格 (见 坐标系)。有了契约就能把任何东西创建成 cold 流 (见 创建),用 pipe 里的纯函数 operator 变换 (见 变换);当值映射出的又是流,就用四种 flatten 组合 (见 高阶),用 Subject / share 决定是否共享执行 (见 冷与热)。这一切最终是为了优雅解决工程里的竞态、防抖、取消、重试 (见 落地)。顺着标准与源头读下去
官方文档 / 教程
- RxJS · Observable 指南 rxjs.dev 官方对 Observable / Observer / Subscription / Subject / operator 的权威说明,marble 与代码对照。
- Learn RxJS learnrxjs.io 按 operator 分类的速查与配方,每条附可运行示例;查「该用哪个 operator」的好去处。
- RxMarbles rxmarbles.com 可交互 marble diagram:拖动源 marble 实时看各 operator 的输出,本系列可视化的灵感来源。
标准接口的源头
-
TC39 · Observable proposal
github.com
把 Observable 纳入 ECMAScript 的提案,定义了
subscribe/ Observer / Subscription 的最小契约。 -
MDN · Observable (DOM)
developer.mozilla.org
WHATWG 把 Observable 引入 DOM:
EventTarget.when()直接返回事件的 Observable,原生的fromEvent。 - MDN · Streams API developer.mozilla.org 对照另一套「流」抽象:ReadableStream 是 pull-based + backpressure,与 Observable 的 push 模型形成互补。