「这是我参与2022首次更文挑战的第24天,活动详情查看:2022首次更文挑战」
这一节来看转换操作符,这些操作符的作用是将一个数据流转化为另一个数据流,Rxjs 的核心就是流的处理,转换处理是非常常见的,因此这部分内容也非常重要。
map
提到转换操作,首先想到的肯定是 map,Rxjs 中也有 map 操作符,它的作用和用法也和你想的一样,使用 map 可以对流中的每一个数据进行一个映射处理,把原始流转化为新的数据流:
of(1, 2, 3).pipe(map(x => 10 * x)).subscribe(x => console.log(x));
// 10
// 20
// 30
这里的实现很简单,弹珠图如下:
mapTo
mapTo 是一种特殊的 map,它会把流中每一个数据都转化为同一个值,mapTo(a) 的效果与 map(x ⇒ a) 的效果相同:
of(1, 2, 3).pipe(mapTo(x => 'a')).subscribe(x => console.log(x));
// a
// a
// a
虽然这种操作符看起来用处很有限,但是在实际应用中和其他操作符结合使用会很方便。它的弹珠图也很简单:
concatMap、concatMapTo
接下来这几组是 Rxjs 中最重要的操作符,严格来说这几个前面已经见过了。首先来看 concatMap,concatMap 效果等同于 concatAll + map,concatMapTo 自然就是 concatAll + mapTo 了,前面已经接触过了,concatAll 是用来处理高阶 Observable 的,它利用 concat 策略将其展开成为低阶 Observable。通常在实际开发中,我们接下来会处理这个低阶 Observable,此时需要进行 map 操作,这两步合起来就是 concatMap。concatMap 的弹珠图:
concatMapTo 的弹珠图:
mergeMap、mergeMapTo
除了把 concat 改为 merge,其他都是一样的,mergeMap 相当于是 mergeAll + map,mergeMapTo 相当于是 mergeAll + mapTo。弹珠图:
switchMap、switchMapTo
switch 与另外两个也是同样道理,switchMap 相当于是 switchAll + map,switchMapTo 相当于是 switchAll + mapTo。弹珠图:
exhaustMap
exhaust 没有 mapTo 操作,exhaustMap 也相当于是 exhaustAll + map 的行为,弹珠图:
上面的四组操作符 concatMap、mergeMap、switchMap 和 exhaustMap 在实际开发中非常常用,我们需要重点理解它们几个的区别,在合适的时机选择正确的操作符,这里看一下在真实开发中的使用。
在 web 开发中经常需要发送网络请求,现在有三个网络请求,我们可以这样写:
const req$ = from(['/a', '/b', '/c']).pipe(map(url => ajax(url)));
这里我们生成了一个高阶 Observable 对象,对应了三个不同的网络请求,想进一步获取请求结果就需要将 Observable 铺平处理,这里可以使用 concatMap 或 mergeMap 来替换 map,区别就在与请求是串行发送还是并行发送:concatMap 是前一个完成后执行下一个,因此如果使用 concatMap,请求是串行发送,而 mergeMap 是根据事件来进行合并,三个请求是同时产生的,这样会并行发送。
再看一个数据竞争的问题,还是一个很常见的场景,左边一个数据列表,点击特定的列表项时根据 id 请求资源展示在右边,这时有一个典型的数据竞争问题:有 a,b 两项,我先点击 a 在 a 请求还未返回时点击 b,b 请求先返回然后再返回 a 的请求结果,这时右侧的数据就会被后返回的 a 请求结果覆盖,但是此时预期应该是显示 b 的内容。对于这个场景,我们通常会通过判断 id 来处理,但是在 Rxjs 中,我们就可以更容易的解决这种问题:
fromEvent(div, 'click').pipe(switchMap(e => ajax('/a?id=' + e.id)));
这里使用了 switchMap,我们知道 switchMap 的策略是当有新内容时舍弃旧内容,这样上面的例子中,点击 b 时 a 的请求就已经被舍弃掉了,这样就不会出现数据竞争问题。
再看一个轮询的场景,我们 500ms 向服务器发一次请求,使用 Rxjs 来实现也很容易:
interval(500).pipe(switchMap(() => ajax('/url')));
这里我们是使用 switchMap 来实现的,每 500ms 发送一次请求,我们只取最新的结果数据,看起来是没问题的,但是考虑一个场景,如果请求响应 500ms 没返回呢?按照 switchMap 的策略,新的发出时旧的就全部舍弃,这样即使 510ms 服务器返回也没有用了,所有超过 500ms 返回的接口返回结果都拿不到,如果这个接口本来就很慢,就可能永远拿不到数据。这是一个我之前在生产环境中发现过的问题,这里最简单的解决方案就是把 switchMap 换成 exhaustMap,exhaustMap 前一个流一旦开始就不会舍弃,这样可以确保已经发出去的请求能够正常获取到结果:
interval(500).pipe(exhaustMap(() => ajax('/url')));
上面几个是常见的 xxxMap 操作符的应用场景,在生产中使用这些操作符的场景还有很多,正确区分和选择是一个非常重要的能力。
本节介绍了部分转换操作符,重点看了 xxxMap 系列操作符的应用,转换操作符还有很多,接下来还会继续学习。