落地:这些问题,用流怎么解决
前面把契约、operator、展平都拆过了。本页把它们对上真实工程痛点,每个都是命令式写法会写得很别扭、而用流几行就说清的场景。
1 · autocomplete 的抖动与竞态
输入框每次改动都要查询后端。命令式写法有两个坑:抖动(每敲一个字符都发请求)与竞态(先发的慢请求晚返回,把过期结果盖在新结果上)。流的解法是 fromEvent(input) → debounceTime → distinctUntilChanged → switchMap(search)。
这两个坑要分开演示:抖动在源头就被 debounceTime 合掉了,一旦开着防抖,快速输入根本发不出多个并发请求,也就看不到竞态。图 1-1 的自动演示因此先关掉防抖,让每个词都真发一次请求,再对比 naive 与 switchMap 两栏。
switchMap 退订旧请求。演示故意让越短的词延迟越久:naive 一栏里早发的短词会最后返回、覆盖正确结果,switchMap 一栏只留最新那条。可自行开关防抖对比两个坑。2 · 拖拽的三事件组合
拖拽是三个事件流的组合:一次按下开启一段移动,直到松开结束。用 switchMap 把每次按下映射成一条移动流,再用 takeUntil 给它自动收尾,不必手动 addEventListener 与 removeEventListener,也不会漏解绑。
本页的实现走的是 Pointer Events(pointerdown / pointermove / pointerup)而非 mouse 事件,为的是拿到 setPointerCapture:指针一旦被捕获,即使拖出元素边界也仍然收得到 pointermove,省掉在
document 上挂全局监听这一步。叙述里说「按下、移动、松开」时,对应的就是这三个 pointer 事件。
3 · 失败后的退避重订阅
Observable 的可取消与可重订阅让重试变得直接:出错时不必手写循环与计时器,retry 会在 error 后重新订阅上游。
有一个容易记错的细节:retry({ count, delay }) 的 delay 传数字时是固定延迟,不会逐次加倍。要指数退避得把 delay 传成函数,让它按重试次数返回一个 timer:
retry({
count: 3,
delay: (err, retryCount) => timer(200 * 2 ** (retryCount - 1)),
})
delay 函数给出。可重跑观察每次重订阅的时点。4 · 问题到 operator 的速查
| 要解决的问题 | operator 组合 |
|---|---|
| 输入抖动:每敲一下都发请求 | debounceTime(静默后发末值)或 auditTime(定时发末值) |
| 需要立刻响应首次操作、其后限流 | throttleTime(默认 leading,发首值) |
| 重复值:内容没变也重复请求 | distinctUntilChanged |
| 请求竞态:过期响应盖掉新结果 | switchMap |
| 并行任务收集全部结果 | mergeMap + toArray,或 forkJoin |
| 严格按序处理 | concatMap |
| 防连点:提交中忽略新点击 | exhaustMap |
| 事件的生命周期与解绑 | takeUntil / take / takeWhile |
| 失败自动重试与退避 | retry({ count, delay }) |
| 共享一次请求给多个订阅者 | share / shareReplay |
retryWhen 也能做重试,但它已被标记废弃,将在 v9 或 v10 移除,官方给的迁移写法是把 retryWhen(() => notify$) 换成 retry({ delay: () => notify$ }) [1]。
建议 · 这些场景的共同点是把「随时间到来的事件」当成一条可组合、可取消的流:抖动用 debounceTime 合并、竞态用 switchMap 退订、生命周期用 takeUntil 收尾、失败用 retry 重订阅。全都建立在最初那个契约的三个能力之上——惰性、多值、可取消。
5 · 参考文献
- RxJS Documentation.
retryWhen(废弃说明与迁移写法)、retry(RetryConfig.delay的两种形态). ReactiveX. - RxJS Documentation.
throttleTime(默认{ leading: true, trailing: false })、auditTime. ReactiveX.