展平:一个值映射成一条流,四种策略
到变换一页为止,map 的结果都是普通值。可一旦 map 的返回值本身又是一个 Observable——比如每个搜索词映射成一次网络请求——就得到一个高阶 Observable(流的流)。要拿到里面真正的值,必须把它展平成一条流。难点在于内层的多条流在时间上会重叠,该保留谁。
1 · 内层重叠时保留谁
四种策略的分歧只在这一处。mergeMap 全都留下,内层并发不设上限;concatMap 让它们排队,一条结束才订阅下一条;switchMap 只留最新的,新值到来就退订上一条;exhaustMap 只留最早的,忙着的时候直接丢掉新值。
| 策略 | 内层重叠时 | 并发 | 会取消或丢弃吗 | 典型用途 |
|---|---|---|---|---|
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 · 参考文献
- RxJS Documentation.
fromFetch(内部AbortController与「以 promise 为输入时退订不会 abort」的说明). ReactiveX.