cover_image

RxJS入门与实践

豆花饲养员 字节大力智能技术团队
2021年10月13日 03:55

作者:大力智能技术团队-前端 豆花饲养员

写在最前面:

(1) 刚入行时有过一年半Angular的开发经验,当时是做数据看板平台,需要处理看板和图表之间的复杂状态管理,而Angular内置的RxJS数据流管理配合依赖注入的强大特性完美cover住复杂数据流。

(2) 在当下React技术栈开发过程中也使用到RxJS,主要用来做活动流程设计、基础组件数据流管理、跨层级组件通信等,使用数据驱动解耦逻辑。

一、引言

1.1 背景

  • 编程范式

命令式编程:通过语句或命令修改程序状态。(eg:JS、Java、Python)

函数式编程:函数是第一等公民,通过函数调用、组合等完成复杂操作。(eg:科里化、函数组合)

声明式编程:描述程序逻辑而非控制流。(eg:SQL、HTML、CSS)

响应式编程:基于数据流及其变化传播的声明性编程范式。(eg:ReactUI = f(state)

Rx (Reactive Extensions):响应式扩展 RxJS(Reactive Extensions for JavaScript):JS的响应式扩展

  • RxJS能做什么?

Think of RxJS as Lodash for events.

将同步/异步操作统一抽象到流,关注数据流的变更,提供对数据流的一系列操作和处理方法。通过数据流驱动,有效降低代码逻辑耦合。

1.2 前置概念

1.2.1 流

In computer science, a stream is a sequence of data elements made available over time.

A stream can be thought of as items on a conveyor belt being processed one at a time rather than in large batches.

流是一系列随时间到达的数据。例如:事件流、直播数据流、文本编辑流、WebSocket。

图片

流可抹平同步和异步差异,异步和同步数据都可放入流中。

图片

1.2.2 观察者模式

定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

图片

✅优点

  • 抽象耦合,可实现UI层和数据层分离;
  • 抽象数据接口,定义稳定的消息传播机制;
  • 支持1对多广播通信

❌缺点

  • 观察层级较深时,广播通信成本高;
  • 观察者和目标之间存在循环依赖时,将导致死循环崩溃;
  • 观察者仅感知目标变化,不感知其知变化过程

二、核心概念

图片

2.1 Observable & Observer

  1. Observable

Observables are lazy Push collections of multiple values.

Observables是多个值的懒推送集合。

  • 多个值:可推送多个值
  • 懒推送:在被第一次订阅时才推送值

图片

  1. Pull vs Push

Pull: 数据消费者主动获取数据,数据生产方无感知。

Push:数据生产方决定发送数据给消费者的时机,数据消费者无感知。


单值多值
PullFunctionIterator
PushPromiseObservable
  • Function 调用时同步返回单个值;
  • Iterator 迭代式调用,调用时返回多个值;
  • Promise 异步返回单个值;
  • Observable 同步/异步地返回多个值。
  1. Observer

An Observer is a consumer of values delivered by an Observable.

Observer是Observable的数据消费者。

图片

Observer就是普通对象,类型定义如下:

interface Observer<T> {
    /** 是否已关闭 */
    closed?: boolean;
    /** 流发出值时执行 */
    next: (value: T) => void;
    /** 流出错时执行 */
    error: (err: any) => void;
    /** 流完成时执行 */
    complete: () => void;
}

// 订阅流
// observable.subscribe(observer);
  1. Subscription

A Subscription is an object that represents a disposable resource, usually the execution of an Observable.

订阅关系代表一个可关闭的资源(通常是Observable的执行)。

图片

// 订阅流
const subscription = stream$.subscribe({
    next: (val) => console.info,
    error: console.error,
    complete: () => console.info('completed'),
});

// 清理订阅
subscription.unsubscribe();

❗️❗️❗️订阅完成后必须调用unsubscribe()以释放资源,防止内存泄露。

2.2 Subject

A Subject is like an Observable, but can multicast to many Observers. Subjects are like EventEmitters: they maintain a registry of many listeners.

Subject是一种支持多播的Observable。

图片

  • 既是Observable:可以直接订阅以获取数据
  • 也是Observer:可以调用next(v)error(e)complete()方法,将被广播至所有注册的Observer。
Subject类型特点示意图
Subject* 普通Subject
* 支持多播

BehaviorSubject* 随时间变化的数据流
* 缓存当前值
* 被订阅时会立即推送最新值
图片
ReplaySubject* 缓存最近的若干值
* 可设置缓存窗口
图片
AsyncSubject* 仅当完成时发出值图片

2.3 Operator

Operators are the essential pieces that allow complex asynchronous code to be easily composed in a declarative manner.

操作符是声明式处理复杂异步的重要基石。

📌 RxJS ships with operators. —— Ben Lesh

RxJS中操作符都是纯函数。官方提供的操作符基本上够日常开发使用,如果有特殊需求可以自定义操作符。

  1. 管道操作符

接收源Observable作为参数,返回新Observable。纯操作,即不改变源Observable。

图片

管道操作符类别参考👉。

类别说明📖举例🌰
组合连接来自多个流的信息。基于值的顺序、时间和结构选择相应操作符。merge 合并流 concat 连接流
转换基于源Observable的值转换为新Observable。map 转换流的值 switchMap 切换流
过滤过滤源Observable的值。filter 过滤流的值 debounce 防抖
多播单值流转换为多播流。multicast 使用Subject多播流
错误处理处理流中的错误。retry 错误重试 catchError 捕获错误
工具工具函数操作。tap 副作用(调试用) timestamp 打印流的值和时间戳
条件对源Observable的条件处理。every 流每个值满足条件时返回true isEmpty 流是否为空
聚集汇聚流的值得到新流。count 计数 reduce 类似数组reduce
  1. 创建操作符

用来创建Observable的函数,如

  • of:按顺序发出任意数量的值
图片
  • interval:基于给定时间间隔发出数字序列
图片
  • fromEvent:将DOM事件转化为Observable
  • fromPromise:将Promise转化为Observable
  • ...

2.4 Scheduler

Scheduler controls when a subscription starts and when notifications are delivered.

调度器控制订阅开始时机、流的数据传播时机。

  1. 调度器提供:
  • 数据存储和消息队列调度
  • 订阅执行上下文
  • 虚拟时钟
  1. 调度器类别

💡在大部分场景下,使用默认调度器即可。

类别说明📖举例🌰
null默认调度器同步、递归(常用)
queueScheduler队列调度器同步、队列
asapScheduler微任务调度器异步、微任务
asyncScheduler异步调度器异步、宏任务
animationFrameScheduler动画调度器window.requestAnimationFrame

三、简单实践

3.1 拖拽小球

StackBlitz项目rxjs-drag

  1. 拖拽过程

小球一次拖拽过程可描述为:

图片

  • 鼠标按下(mousedown):拖拽开始,记录鼠标按下位置
  • 鼠标移动(mousemove):记录小球初始位置,在鼠标移动时更新小球位置
  • 鼠标弹起(mouseup):本次拖拽结束
  1. 数据流

拖拽过程中触发的鼠标事件对应三个流:鼠标按下mousedown$、鼠标移动mousemove$、鼠标弹起mouseup$

图片

拖拽流drag$可描述如下:

  • mousedown$触发拖拽,记录鼠标按下位置
  • 切换(switchMap)至mousemove$,记录小球初始位置,并在移动过程中计算小球移动位置
  • mouseup$触发时,终止(takeUntil)mousemove$
const mousedown$ = fromEvent<MouseEvent>(ball, 'mousedown').pipe(
  map(getMouseEventPos)
);

const mousemove$ = fromEvent<MouseEvent>(document'mousemove').pipe(
  map(getMouseEventPos)
);

const mouseup$ = fromEvent<MouseEvent>(document'mouseup');

const drag$ = mousedown$.pipe(
  switchMap(initialPos => {
    const { top, left } = ball.getBoundingClientRect();

    return mousemove$.pipe(
      map(({ x, y }) => ({
        top: y - initialPos.y + top,
        left: x - initialPos.x + left
      })),
      takeUntil(mouseup$)
    );
  })
);

小球订阅drag$,更新自身位置。

drag$.subscribe(({ top, left }) => {
  ball.style.top = `${top}px`;
  ball.style.left = `${left}px`;
  ball.style.bottom = '';
  ball.style.right = '';
});

3.2 toast管理

StackBlitz项目rxjs-toast

  1. toast需求

一次toast可描述如下:(约定toast停留时长为3s,且不支持手动隐藏)

  • 展示一段文本
  • 3s内无新toast出现,自动隐藏
  • 3s内有新toast出现,重新计时3s,并覆盖老toast

图片

  1. 数据流

将toast管理拆分为:

  • 展示show$:触发展示
  • 隐藏hide$:到达停留时长,或新toast覆盖当前

图片

const click$ = fromEvent(document.getElementById('btn'), 'click');
const toast$ = click$.pipe(
  switchMap(() => {
    let hideByDuration = false;

    const duration$ = timer(2000).pipe(
      mapTo('hide by duration'),
      tap(() => (hideByDuration = true))
    );

    return concat(of('show'), duration$).pipe(
      finalize(() => {
        if (!hideByDuration) {
          console.log('hide by next');
        }
      })
    );
  })
);

toast$.subscribe(console.info);

四、结语

RxJS在流程控制、多个异步协作等场景中均表现良好,使用RxJS写出的代码简洁高效。希望大家在技术选型时可多多考虑RxJS。

参考文档

  • ReactiveX
  • RxJS官网
  • 学习RxJS操作符
  • 图解设计模式


点击阅读原文,了解更多技术干货~

继续滑动看下一个
字节大力智能技术团队
向上滑动看下一个