Web 平台 API / RxJS · 从 Observable 标准契约,到它解决的那些问题 / 落地:这些问题,用流怎么解决 待审核 7 / 7
applications · 竞态 / 防抖 / 拖拽 / 重试

落地:这些问题,用流怎么解决

前面把契约operator展平都拆过了。本页把它们对上真实工程痛点,每个都是命令式写法会写得很别扭、而用流几行就说清的场景。

1 · autocomplete 的抖动与竞态

输入框每次改动都要查询后端。命令式写法有两个坑:抖动(每敲一个字符都发请求)与竞态(先发的慢请求晚返回,把过期结果盖在新结果上)。流的解法是 fromEvent(input)debounceTimedistinctUntilChangedswitchMap(search)

这两个坑要分开演示:抖动在源头就被 debounceTime 合掉了,一旦开着防抖,快速输入根本发不出多个并发请求,也就看不到竞态。图 1-1 的自动演示因此先关掉防抖,让每个词都真发一次请求,再对比 naive 与 switchMap 两栏。

图 1-1 · 左右对照直接发请求与 switchMap 退订旧请求。演示故意让越短的词延迟越久:naive 一栏里早发的短词会最后返回、覆盖正确结果,switchMap 一栏只留最新那条。可自行开关防抖对比两个坑。

2 · 拖拽的三事件组合

拖拽是三个事件流的组合:一次按下开启一段移动,直到松开结束。用 switchMap 把每次按下映射成一条移动流,再用 takeUntil 给它自动收尾,不必手动 addEventListenerremoveEventListener,也不会漏解绑。

本页的实现走的是 Pointer Events(pointerdown / pointermove / pointerup)而非 mouse 事件,为的是拿到 setPointerCapture:指针一旦被捕获,即使拖出元素边界也仍然收得到 pointermove,省掉在 document 上挂全局监听这一步。叙述里说「按下、移动、松开」时,对应的就是这三个 pointer 事件。

图 2-1 · 按住方块拖动时三条事件流的组合,读数给出本次拖拽的移动事件计数。可拖出元素边界观察指针捕获的效果。

3 · 失败后的退避重订阅

Observable 的可取消与可重订阅让重试变得直接:出错时不必手写循环与计时器,retry 会在 error 后重新订阅上游。

有一个容易记错的细节:retry({ count, delay })delay 传数字时是固定延迟,不会逐次加倍。要指数退避得把 delay 传成函数,让它按重试次数返回一个 timer

retry({
  count: 3,
  delay: (err, retryCount) => timer(200 * 2 ** (retryCount - 1)),
})
图 3-1 · 一个前两次失败、第三次成功的请求,退避间隔 200ms 与 400ms 由上面那个 delay 函数给出。可重跑观察每次重订阅的时点。

4 · 问题到 operator 的速查

抖动一行的三个 operator 不可互换:debounceTimeauditTime 取窗口末值,throttleTime 默认取窗口首值,输入框场景要的是末值。
要解决的问题 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 · 参考文献

  1. RxJS Documentation. retryWhen(废弃说明与迁移写法)、retryRetryConfig.delay 的两种形态). ReactiveX.
  2. RxJS Documentation. throttleTime(默认 { leading: true, trailing: false })、auditTime. ReactiveX.