重学 Rxjs —— 组合操作符

1,086 阅读6分钟

「这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战

组合操作符的作用是把多个数据流聚合或连接起来的操作符,这种组合操作在数据处理中很常见,不同的操作符的组合策略各不相同,本节看一下这部分内容。

concat、concatWith、concatAll

在看 concatWith 之前先来看一下静态的 concat 函数,concat 意思是连接,就是把多个数据流按照次序连接起来,前一个数据流结束之后再开启下一个数据流,我们先看一个例子:

concat(of(1, 2), of(3, 4)).subscribe(x => console.log(x));
// 1
// 2
// 3
// 4

使用弹珠图来描述也很清晰:

image.png

这里的 concat 是一个函数但是不是操作符,操作符应该是以一个数据流作为主体来调用,实现 concat 效果的操作符叫做 concatWith(concat 操作符已过时不建议使用),使用 concatWith 上面的例子可以转化为等价的写法:

of(1, 2).pipe(concatWith(of(3, 4))).subscribe(x => console.log(x));
// 1
// 2
// 3
// 4

可以看到操作符的写法和静态函数的写法效果是相同的,它们的区别主要在使用场景上,静态函数通常用于在较前置的流程中创建原始数据流,而操作符用于处理过程中,可以与其他操作符进行组合,实现复杂的转换效果。这里包括但不限于 concat 和 concatWith,后面会看到其他几组操作符都是这样的。

concatAll 和接下来要见到的几个 xxxAll 都是用来处理高阶 Observable 的操作符,高阶 Observable 指的是内容为 Observable 的 Observable,举个例子:

of(of(1), of(2), of(3));

上面的就是一个高阶 Observable,在实际开发中高阶 Observable 非常常见。直接订阅它我们收到的还是 Observable,需要再次订阅才能拿到实际数据,这样处理起来很不方便,因此对于高阶 Observable 我们通常都会先将其转化为低阶 Observable 再处理,这个转换的过程就是对高阶 Observable 内部的所有 Observable 进行一个组合,xxxAll 系列操作符实现的就是这种组合效果。

现在再来看 concatAll,它的意思就是以 concat 的策略去处理高阶 Observable,即对高阶 Observable 中所有的 Observable 对象按照次序连接起来,从弹珠图上面看效果更加直观:

image.png

我们可以尝试一下,使用 concatAll 处理上面的例子:

of(of(1), of(2), of(3)).pipe(concatAll()).subscribe(x => console.log(x));
// 1
// 2
// 

merge、mergeWith、mergeAll

merge 是合并,它的组合策略是按照数据产生的时间顺序进行合并,举个例子:

merge(fromEvent(document, 'click'), interval(1000)).subscribe(x => console.log(x));

上面合并了点击和定时器两个数据流,当点击时会输出点击事件,当定时器触发时会输出数字,哪个产生就打印哪个,我们直接看弹珠图更直观:

image.png

这里可以与 concat 对比理解,merge 是按照时间顺序合并,与数据流是否结束无关。类似的也有 mergeWith(merge 操作符并未标记为过时,但是还是更建议使用 mergeWith) 和 mergeAll 操作符。

这里看一下 mergeAll,它的弹珠图如下:

image.png

switchAll

除了 concatAll 与 mergeAll,还有 switchAll 和 exhaustAll 两个处理高阶 Observable 的操作符,而高阶 Observable 往往是由 map 操作产生的,因此这几个操作符通常与 map 共同使用,还提供了简化版本的 xxxMap 系列操作符,这一部分将在下一节转换操作符中详细介绍,这里重点先看一下几种策略的区别。

先来看 switchAll,它的处理策略是优先切到新的 Observable 上,只选择最新的 Observable 上面的数据舍弃旧数据。从弹珠图上来看更直观:

image.png

exhaustAll

exhaustAll 的处理策略从某种意义上说是与 switchAll 相反的,switchAll 是倾向切换到新的 Observable 上,而 exhaustAll 是倾向于保持在当前 Observable 上。只要当前 Observable 未结束,那么这段时间都只取当前 Observable 上的数据,舍弃这段时间其他 Observable,直到当前 Observable 结束才切到下一个 Observable 上,对比弹珠图可以理解这个差异:

image.png

race、raceWith

race 意思是竞速,因此这个非常容易理解,就是取最早产生数据那个 Observable,舍弃剩下的 Observable,直接看弹珠图:

image.png

同样这里的 race 是一个普通函数,与之对应的操作符是 raceWith(race 操作符已标记过时),二者只是写法和使用场景上的差异。

combineLatest、combineLatestWith、combineLatestAll

combineLatest 的作用是收集最新的值,它的效果是每当 Observable 新产生值时,把当前所有 Observable 的最新值组合发出,举个例子:

combineLatest([timer(0, 1000), timer(500, 1000)]).subscribe(v => console.log(v));
// [0, 0] after 0.5s
// [1, 0] after 1s
// [1, 1] after 1.5s
// [2, 1] after 2s

使用弹珠图描述:

image.png

对应的操作符为 combineLatestWith(combineLatest 操作符已过时),此外对于高阶 Observable 也有对应的处理操作符 combineLatestAll。

zip、zipWith、zipAll

zip 也是组合 Observable 的操作符,它的行为很简单,就是逐一组合:

zip(of(27, 25, 29), of('Foo', 'Bar', 'Beer'), of(true, true, false)).subscribe(x => console.log(x));
// [27, 'Foo', true]
// [25, 'Bar', true]
// [29, 'Beer', false]

类似的也有 zipWith 和 zipAll 操作符。

withLatestFrom

withLatestFrom 与上面的 combineLatest 看起来很像,都是发出流中的最新值,区别在于 withLatestFrom 有一个主 Observable,只有主 Observable 的数据变化时才会触发流,先看弹珠图:

image.png

对比 combineLatest,这里只有第一个流产生数据触发了流的变化,其他流只会影响最新值,并不会影响发生时机,举个例子:

fromEvent(document, 'click').pipe(withLatestFrom(interval(1000))).subscribe(x => console.log(x));

这里只有真正点击时会打印,打印的内容中可以看到当前计时器的最新数据。

forkJoin

forkJoin 也是进行流的组合,它是当所有流都结束后触发,内容为组合所有的流结束时的最终的值,因此 forkJoin 只会产生一个结果,然后直接结束流:

forkJoin([
  of(1, 2, 3, 4),
  of(8),
  timer(4000),
]).subscribe(x => console.log(x));
// [4, 8, 0] after 4s

这个例子中,4 秒之后全部的流都结束,此时最新的值分别为 4、8、0。forkJoin 的弹珠图:

image.png

startWith

这个非常简单,就是在流的最开始添加内容:

of(1, 2, 3).pipe(startWith(0)).subscribe(x => console.log(x));
// 0
// 1
// 2
// 3

使用弹珠图描述:

image.png

endWith

endWith 与 startWith 相反,它是在流结束后添加内容,如果流不结束 endWith 不会触发:

of(1, 2, 3).pipe(endWith(0)).subscribe(x => console.log(x));
// 1
// 2
// 3
// 0

弹珠图:

image.png

以上这些就是常用的组合操作,在实际开发中组合是一个很基础的行为,尤其是在处理高阶 Observable 时,选择正确的组合策略非常重要。在组合之后往往伴随着转换行为,将原始数据转化为想要的内容。