RxJS 前端最佳实践

Tags
Web Dev
RxJS
Published
December 22, 2021
Author
HJS
在前端框架如此丰富的今天,RxJS 在其中能够扮演什么角色。这篇文章不会涉及 RxJS 的基础概念和知识,如果你不熟悉建议从官方文档开始😅。
 

前端编程是异步的

前端编程本质上是异步的,通常异步事件可以归为以下 3 类:
  • 用户事件,例如 Click 事件等
  • Remote API,例如 Fetch、WebSocket 等
  • 运行环境的异步 API,例如 setTimeout、setInterval 等
 
现代前端框架通常提供了声明式绑定用户事件的能力,但仅凭这点想描述复杂异步编程显得有些苍白。例如,让我们设计一个轮播图,它应当具有以下功能:
  • 每次播放之间应该要有短暂时间停顿(节流)
  • 点击指示器跳转播放
    • 点击时会立即触发指示器的动画效果
  • 用户拖动播放
  • 用户键盘 arrow 事件播放
    • 缓存最近的 10 次键盘 arrow 事件并播放
  • 自动计时播放
    • 用户鼠标进入轮播图范围停止自动播放,离开启动自动播放
    • 自动计时播放在用户拖动、点击、arrow 键盘事件播放后要重新计时
 
想象一下假如在 React 框架中,现在的代码会充满糟糕的味道,例如:
  • 由于前端框架中用户事件和其他异步事件的上下文割裂,需要维护 N 个状态在各个异步事件间通信,即使引入状态机也只是增加部分可读性。
  • 由于判断用户拖动涉及多个用户事件配合,需要将内聚的逻辑强行拆分到不同合成事件中,或引入其它第三方库。
  • 由于前端框架合成事件只能声明一个,需要把多个依赖同一个合成事件但完全不相关的逻辑强行耦合,放到同一个的合成事件入口。
  • 由于存在节流,缓存等常见事件处理需求,需要引入第三方库或手动维护计时器或队列等结构。
此时代码具有以下特征:代码量大,引入各种第三方库、样板代码多、状态无处不在、不相关的逻辑耦合在同一个入口,内聚的逻辑却被拆分到不同用户事件。
我相信几周后即使是原作者看到这份代码也会头痛,让我们引入 RxJS 中的概念来解决这些问题。
 

RxJS 异步编程概念 - 流(Stream)

"流"用来描述时间维度上一系列的"值"。例如用户和网站交互时的一系列 click 事件,就可以看作一个流。为了让流的概念对构建程序有用,我们需要一种方法来创建流、订阅它们、对新值做出反应、处理流以及将流组合在一起以构建新的流,这就是 RxJS 中 Observable 和 Operator 的概念。
让我们看一个简单的例子
const mousedown$ = fromEvent(ref.current, 'mousedown'); const mousemove$ = fromEvent(ref.current, 'mousemove'); const mouseup$ = fromEvent(ref.current, 'mouseup'); const keydown$ = fromEvent(ref.current, 'keydown') const drag$ = mousedown$.pipe( switchMap(start => mousemove$.pipe( map(cur => cur.clientX - start.clientX), takeUntil(mouseup$), ) ), take(1), repeat() ); const arrow$ = keydown$.pipe( filter(e => e.key === 'ArrowRight') bufferCount(10) ) const interval$ = interval(5000).pipe( takeUntil(drag$), takeUntil(arrow$), repeat() ) const play$ = merge(interval$, drag$, arrow$).pipe( throttleTime(200) ); play$.subscribe(() => { play() // 播放下一张 });
代码非常优雅!我相信即使没有学习过 RxJS 的工程师也能推理出代码的行为,如果要系统的归纳出 RxJS 究竟有哪些点不同,那么下面就是结论。
 

RxJS vs. 现代前端框架异步编程

  • 事件流支持合并、中止、重新订阅,强相关逻辑能够内聚在一起。
  • 事件流是能够复用的,依赖同一事件的不同逻辑允许多个观察者,无需在同一入口。
  • 流模型天然适合防抖、节流、缓存等,无需引入第三方库做额外处理,减少样板代码。
  • 对于需要控制事件发生顺序,支持事件的动态订阅和取消,无需引入额外状态。