Nest探索(十)RxJS 入门

675 阅读5分钟

💥 在掘金写技术好文,瓜分万元现金大奖 | 4月金石计划

前言

在学习 Nest 中的 interceptors 时,可以看到很多 RxJS 语法的代码,也可以在 nest 项目中看到 rxjs 的依赖项:

Snipaste_2024-05-08_15-18-07.png

那么 RxJS 是什么呢?

根据官网描述,RxJS 是使用可观察序列(observable sequences)来编写异步的和基于事件的程序的一个库,可以把 RxJS 当作用于处理事件的 Lodash。

Think of RxJS as Lodash for events.

官网说了很多的概念:Observable、Observer、Subscription、Operators、Subject、Schedulers等等。

Snipaste_2024-05-08_15-23-42.png

在有了大概的理解后,从下面几个例子出发去探索学习下,最后,再回头来看相关的概念,便能一目了然。

举个例子

首先,从一个简单的例子出发:假如我们想在点击页面的时候打印 Clicked! ,如何实现呢?

  • JS 实现很简单,注册一个事件监听器即可:
document.addEventListener('click', () => console.log('Clicked!'));
  • 使用 RxJS 的话,是借助 fromEvent 和 subscribe 去实现的:
import { fromEvent } from 'rxjs';

fromEvent(document, 'click')
  .subscribe(() => console.log('Clicked!'));

咋一看有点熟悉,还是有 DOM 对象 document 和相应的点击事件 'click',但是写法明显是不一样的。这里使用了 RxJS 的 fromEvent() 函数,实际上是对于给定的 DOM 对象创建了一个 Observable

那么 Observable 是什么呢?

查看文档可知,Observable 是RxJS 的基础和核心,是一个表示随时间推移的异步事件流的对象。Observable 类上面有很多常用的属性和方法,比如 subscribe(), pipe() 等等。

/**
 * A representation of any set of values over any amount of time. This is the most basic building block
 * of RxJS.
 *
 * @class Observable<T>
 */
export class Observable<T> implements Subscribable<T> {
  /**
   * @constructor
   * @param {Function} subscribe the function that is called when the Observable is
   * initially subscribed to. This function is given a Subscriber, to which new values
   * can be `next`ed, or an `error` method can be called to raise an error, or
   * `complete` can be called to notify of a successful completion.
   */
  constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic) {
    if (subscribe) {
      this._subscribe = subscribe;
    }
  }
    
  subscribe(observerOrNext?: Partial<Observer<T>> | ((value: T) => void)): Subscription;
  subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;
    
  pipe(): Observable<T>;
  pipe<A>(op1: OperatorFunction<T, A>): Observable<A>;
  // ...
}

fromEvent(), subscribe()

在上面的例子,我们可以看到使用了fromEvent(),看文档可知是对于给定的 target 对象创建一个 Observable,当监听到某个事件 eventName 触发时会触发。

export function fromEvent<T>(
  target: JQueryStyleEventEmitter<any, T> | ArrayLike<JQueryStyleEventEmitter<any, T>>,
  eventName: string
): Observable<T>;
export function fromEvent<T, R>(
  target: JQueryStyleEventEmitter<any, T> | ArrayLike<JQueryStyleEventEmitter<any, T>>,
  eventName: string,
  resultSelector: (value: T, ...args: any[]) => R
): Observable<R>;
// Creates an Observable that emits events of a specific type coming from the given event target.

此外,还使用了subscribe(),字面上是订阅的意思,也就是触发 Observable 去执行某个方法。而且触发 subscribe 的实际时机是 Observable 开始工作的时候,并非创建的时候。

subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;
// Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.
// ... calling `subscribe` is actually the moment when Observable starts its work, not when it is created.

subscribe() 可以有 next, error, complete 三个传参,正常情况下,Observable 数据流会传入 next 函数,当结束时会触发 complete 函数。举个例子:

of(10, 20, 30)
  .subscribe(
    value => console.log('next:', value),
    err => console.log('error:', err),
    () => console.log('the end'),
  );
// 打印了:
// next: 10
// next: 20
// next: 30
// the end

pipe(), scan(), filter(), map()

pipe()是 RxJs 提供的一个函数,可以组合多个操作符,以对数据流进行处理。这些操作符会依次对数据流进行处理。

举个例子,如果点击页面的时候同时提示点击次数,可以这样实现:

fromEvent(document, 'click')
  .pipe(scan((count) => count + 1, 0))
  .subscribe((count) => console.log(`Clicked ${count} times`));

scan() 类似于 js 中的 reduce() 函数,传参处理函数和初始值。在上面的代码中,我们对于每次数据流的数值,经过 +1 操作后进行累加。

这里我们不能看出,使用 RxJS 具有一个显著的优点,那就是可以通过使用纯函数隔绝变量的状态,使得代码的健壮性更高。也就是说,这里的 scan 函数的参数是一个纯函数,是在纯函数中去统计点击次数,因此次数 count 不会暴露出来,就不易受到外部的影响。

此外,pipe() 还可以组合更多的操作符:

of(1,2,3,4)
.pipe(
  filter(x => x % 2 === 0),
  map(x => x + x),
)
.subscribe(x => console.log(x)) // 4 8

代码中,我们还可以看到这两个操作符:

  • filter() 操作符:如字面意思那样,可以过滤不符合条件的数据流。
  • map() 操作符:类似于 js 中的 map 函数,可以对于数据流中的每个数值进行处理后再返回数据流中。

interval(), take()

我们可以使用interval(time)实现类似于定时器的效果,每隔时间 time(单位 ms) 触发生成一个非负整数。

接着,使用take(num)进行判断,num 指定了最大的触发值(不包含该数值)。

因此,下面的代码中只会打印出 0 ~ 3 。

const numbers = interval(1000);
const takeFourNumbers = numbers.pipe(take(4));
takeFourNumbers.subscribe(x => console.log('Next: ', x));
// Logs:
// Next: 0
// Next: 1
// Next: 2
// Next: 3

tap(fn)

这个操作符的功能是执行副作用操作,但不会改变数据流。函数 fn 可以接收到数据流中的数值。

举个例子:

of(Math.random())
  .pipe(
    tap(console.log),
    map(n => n > 0.5 ? 'big' : 'small')
  )
  .subscribe(console.log);
// 0.003072992334982372
// small

这里会打印随机数字,并进一步与 0.5 判断大小。

后记

本文介绍了 RxJS 中常用的操作符及其示例,看到这里我们对于 RxJS 语法应该有了一定的掌握了,此时再去结合 nest 中 interceptors 的使用就问题不大了。

参考