RxJS Operators

437 阅读6分钟

RxJS Operators

尽管 Observable 是基础,但 RxJS 对其操作符最有用。运算符是允许以声明方式轻松组合复杂异步代码的基本部分。

What are operators?

运算符是函数。有两种运算符:

Pipeable Operators 是一种可以使用 observableInstance.pipe(operator()) 语法通过管道传输到 Observables 的操作符。其中包括 filter(...) 和 mergeMap(...)。调用时,它们不会更改现有的 Observable 实例。相反,它们返回一个新的 Observable,其订阅逻辑基于第一个 Observable。

Pipeable Operator 是一个将 Observable 作为其输入并返回另一个 Observable 的函数。这是一个纯粹的操作:之前的 Observable 保持不变。

Pipeable Operator 本质上是一个纯函数,它将一个 Observable 作为输入并生成另一个 Observable 作为输出。订阅输出 Observable 也会订阅输入 Observable。

Creation Operators 是另一种运算符,可以作为独立函数调用以创建新的 Observable。例如: of(1, 2, 3) 创建一个 observable,它将一个接一个地发出 1、2 和 3。创建运算符将在后面的部分中更详细地讨论。

例如,名为 map 的运算符类似于同名的 Array 方法。就像 [1, 2, 3].map(x => x * x) 会产生 [1, 4, 9] 一样,Observable 是这样创建的:


import { of } from 'rxjs';

import { map } from 'rxjs/operators';

of(1, 2, 3)

.pipe(map((x) => x * x))

.subscribe((v) => console.log(`value: ${v}`));

// Logs:

// value: 1

// value: 4

// value: 9

将发出 1、4、9。另一个有用的运算符是 first:


import { of } from 'rxjs';

import { first } from 'rxjs/operators';

of(1, 2, 3)

.pipe(first())

.subscribe((v) => console.log(`value: ${v}`));

// Logs:

// value: 1

请注意,map 必须在逻辑上动态构建,因为它必须被赋予映射函数。相比之下, first 可能是一个常量,但仍然是动态构建的。作为一般做法,所有运算符都被构造,无论它们是否需要参数。

Piping

可管道操作符是函数,所以它们可以像普通函数一样使用:op()(obs)——但在实践中,往往有许多它们被卷积在一起,很快变得不可读:op4()(op3()(op2() )(op1()(obs))))。出于这个原因,Observables 有一个名为 .pipe() 的方法可以完成同样的事情,同时更容易阅读:


obs.pipe(op1(), op2(), op3(), op4());

就风格而言,即使只有一个运算符,也不会使用 op()(obs) ; obs.pipe(op()) 是普遍首选的。

Creation Operators

What are creation operators?

与可管道操作符不同,创建操作符是可用于创建具有一些常见预定义行为的 Observable 或通过加入其他 Observable 的函数。

创建运算符的典型示例是区间函数。它接受一个数字(不是 Observable)作为输入参数,并产生一个 Observable 作为输出:


import { interval } from 'rxjs';

const observable = interval(1000 /* number of milliseconds */);

Higher-order Observables

Observables 最常发出普通值,如字符串和数字,但令人惊讶的是,它经常需要处理 Observables 的 Observables,即所谓的高阶 Observables。例如,假设您有一个 Observable 发出字符串,这些字符串是您想要查看的文件的 URL。代码可能如下所示:


const fileObservable = urlObservable.pipe(map((url) => http.get(url)));

http.get() 为每个单独的 URL 返回一个 Observable(可能是字符串或字符串数​​组)。现在你有了一个 Observable 的 Observable,一个高阶的 Observable。

但是如何使用高阶 Observable 呢?通常,通过展平:通过(以某种方式)将高阶 Observable 转换为普通 Observable。例如:


const fileObservable = urlObservable.pipe(

map((url) => http.get(url)),

concatAll()

);

concatAll() 运算符订阅从“外部” Observable 出来的每个“内部” Observable,并复制所有发出的值,直到该 Observable 完成,然后继续下一个。所有的值都以这种方式连接。其他有用的展平运算符(称为连接运算符)是

  • mergeAll() — 在每个内部 Observable 到达时订阅它,然后在它到达时发出每个值

  • switchAll() — 当第一个内部 Observable 到达时订阅它,并在它到达时发出每个值,但是当下一个内部 Observable 到达时,取消订阅前一个,并订阅新的。

  • Exhaust() — 当第一个内部 Observable 到达时订阅它,并在它到达时发出每个值,丢弃所有新到达的内部 Observable,直到第一个完成,然后等待下一个内部 Observable。

就像许多数组库将 map() 和 flat()(或 flatten())组合成一个 flatMap() 一样,所有 RxJS 展平操作符 concatMap()、mergeMap()、switchMap() 和exhaustMap 都有映射等价物()。

Marble diagrams

为了解释运算符的工作原理,文字描述通常是不够的。许多算子都与时间有关,他们可能以不同的方式例如延迟、采样、节流或去抖动值排放。图表通常是更好的工具。大理石图是操作符如何工作的可视化表示,包括输入 Observable(s)、操作符及其参数以及输出 Observable。

有用于不同目的的运算符,它们可以分类为:创建、转换、过滤、加入、多播、错误处理、实用程序等。在下面的列表中,您将找到按类别组织的所有运算符。

列表

Creating custom operators

使用 pipe() 函数创建新的运算符

如果您的代码中有常用的运算符序列,请使用 pipe() 函数将该序列提取到新的运算符中。即使序列不那么常见,将其分解为单个运算符也可以提高可读性。

例如,您可以创建一个丢弃奇数值并将偶数值加倍的函数,如下所示:


import { pipe } from 'rxjs';

import { filter, map } from 'rxjs/operators';

function discardOddDoubleEven() {

return pipe(

filter((v) => !(v % 2)),

map((v) => v + v)

);

}

pipe() 函数类似于 Observable 上的 .pipe() 方法,但与此不同。)

Creating new operators from scratch

它更复杂,但是如果您必须编写一个不能由现有运算符组合而成的运算符(很少发生),您可以使用 Observable 构造函数从头开始编写一个运算符,如下所示:


import { Observable, of } from 'rxjs';

function delay<T>(delayInMillis: number) {

return (observable: Observable<T>) =>

new Observable<T>((subscriber) => {

// this function will be called each time this

// Observable is subscribed to.

const allTimerIDs = new Set();

let hasCompleted = false;

const subscription = observable.subscribe({

next(value) {

// Start a timer to delay the next value

// from being pushed.

const timerID = setTimeout(() => {

subscriber.next(value);

// after we push the value, we need to clean up the timer timerID

allTimerIDs.delete(timerID);

// If the source has completed, and there are no more timers running,

// we can complete the resulting observable.

if (hasCompleted && allTimerIDs.size === 0) {

subscriber.complete();

}

}, delayInMillis);

allTimerIDs.add(timerID);

},

error(err) {

// We need to make sure we're propagating our errors through.

subscriber.error(err);

},

complete() {

hasCompleted = true;

// If we still have timers running, we don't want to yet.

if (allTimerIDs.size === 0) {

subscriber.complete();

}

},

});

// Return the teardown logic. This will be invoked when

// the result errors, completes, or is unsubscribed.

return () => {

subscription.unsubscribe();

// Clean up our timers.

for (const timerID of allTimerIDs) {

clearTimeout(timerID);

}

};

});

}

// Try it out!

of(1, 2, 3).pipe(delay(1000)).subscribe(console.log);

请注意,您必须

1.在订阅输入 Observable 时实现所有三个 Observer 函数 next()、error() 和 complete()。

2.实现一个“拆卸”功能,在 Observable 完成时进行清理(在这种情况下,通过取消订阅和清除任何挂起的超时)。

3.从传递给 Observable 构造函数的函数中返回该拆卸函数。

当然,这只是一个例子; delay() 运算符已经存在。