Web 平台 API / RxJS · 从 Observable 标准契约,到它解决的那些问题 / 展平:一个值映射成一条流,四种策略 待审核 5 / 7
flattening · mergeMap / concatMap / switchMap / exhaustMap

展平:一个值映射成一条流,四种策略

变换一页为止,map 的结果都是普通值。可一旦 map 的返回值本身又是一个 Observable——比如每个搜索词映射成一次网络请求——就得到一个高阶 Observable(流的流)。要拿到里面真正的值,必须把它展平成一条流。难点在于内层的多条流在时间上会重叠,该保留谁。

1 · 内层重叠时保留谁

四种策略的分歧只在这一处。mergeMap 全都留下,内层并发不设上限;concatMap 让它们排队,一条结束才订阅下一条;switchMap 只留最新的,新值到来就退订上一条;exhaustMap 只留最早的,忙着的时候直接丢掉新值。

图 1-1 · 同一条外层输入(三个查询 a / b / c 在不同时刻到达,每个映射成一条持续 3 拍的内层流)并排跑四种策略,marble 的颜色标出它来自哪个查询。可推进时间游标看四条下游何时分道扬镳。
图 1-2 · 四种策略的订阅与退订时序。可逐条对照「新内层到达时对旧内层做了什么」。
「并发」一栏指同时活着的内层订阅数。mergeMap 并非一次性订阅全部内层,而是每个外层值到达时才订阅它对应的那一条,已订阅的都不退订。
策略 内层重叠时 并发 会取消或丢弃吗 典型用途
mergeMap 全部保留 无上限 都不,全保留 独立并行任务
concatMap 排队等待 1(串行) 不丢,但会阻塞 严格保序
switchMap 退订旧的 1(最新) 退订旧内层 autocomplete、切换
exhaustMap 忽略新的 1(最早) 丢弃新值 防重复提交

2 · switchMap 与请求竞态

用户快速改动输入,每次改动都发一次请求。若用 mergeMap,旧请求的响应可能晚于新请求返回,把过期结果覆盖上去,这就是请求竞态switchMap 在新值到来时退订上一条内层订阅、丢弃它后续的所有值,永远只保留最新那条。

「退订」与「取消在途请求」是两件事,值得单独说清。退订一定会运行内层的 teardown,但那个 teardown 具体做什么由内层 Observable 自己决定:fromFetch 内部挂了 AbortController,退订会真的 abort;ajax 走 XHR 的 abort(),也会。而 from(fetch(...))from(promise) 这类把 promise 包成 Observable 的写法不会——RxJS 文档明确写着,退订任何以 promise 为输入的 Observable 都不会中止该请求 [1],HTTP 请求照跑到底、响应照样落地,只是被丢弃。竞态在两种情况下都被解决了(因为过期值不再流向下游),但省不省流量是另一回事。

建议 · 选型只看一个问题:内层重叠时该保留谁。要全部结果用 mergeMap,要严格保序用 concatMap,要「只认最新」用 switchMap,要「一次只做一件、期间的点击当没发生」用 exhaustMap落地一页有 autocomplete 的可交互 demo。

3 · 参考文献

  1. RxJS Documentation. fromFetch(内部 AbortController 与「以 promise 为输入时退订不会 abort」的说明). ReactiveX.