关于 switchMap 及其相关
RxJS附带 100 多个不同的操作符。SwitchMap可能是讨论最多的一个。它是一个非常强大的操作符,在很多情况下都非常有用,但同时也相当危险。在这篇博客文章中,我们将讨论switchMap
并介绍它可能给你的应用程序带来的问题。此外,我们还将介绍具有类似用例的操作符。这将帮助你在下次需要选择其中之一时更加轻松。
在讨论使用 SwitchMap 的注意事项之前switchMap
,我们先来总结一下它switchMap
的工作原理。SwitchMap 就是所谓的高阶运算符。你可能已经熟悉高阶函数的概念,因为它们非常相似。
如果你不熟悉,高阶函数是一个返回另一个函数或接受一个函数作为参数的函数。想想 Array 方法map
。它接受一个函数作为参数,该函数用于定义数组中每个元素的变换。
高阶运算符正在处理 Observable 的 Observable。SwitchMap
特别是返回内部 Observable 的通知。
of('hello world').pipe(
switchMap(value => {
return ajax.getJSON('http://my.api.com?search=' + value);
}),
);
此代码示例将用于switchMap
粗略解释。稍后我们将对其进行扩展,以便更深入地了解它。
但首先要做的事情是:从外部可观察对象(由ofSwitchMap
运算符返回)获取值,并将其作为参数传递给一个必须返回新可观察对象的函数。在本例中,我们使用了 RxJS 的 ajax 模块(您可以在此处找到一些相关信息)。在这里,我们使用该方法执行 get 请求,并将其响应作为新的可观察对象返回。getJSON
从技术上讲,我们正在订阅新返回的可观察对象并将其值传递给链中的下一个操作符,或者像往常一样传递给订阅方法中的下一个处理程序。
现在您已经熟悉了 的基础知识switchMap
,我们将对其进行更深入的研究。如果您需要一些时间才能完全理解switchMap
细节,也不用担心。如果您理解了,您会注意到concatMap
、mergeMap
和exhaustMap
非常相似。但首先,让我们深入研究switchMap
。正如承诺的那样,我扩展了上面的示例,并且为了使其更易于探索,我为其
创建了一个Blitz
。 在 中index.ts
,您会偶然发现以下代码片段。
const httpCall$ = ajax.getJSON('https://rickandmortyapi.com/api/character/');
const click$ = fromEvent(document, 'click');
const switchMapExample$ = click$.pipe(
tap(() => console.log('inside switchMap - click happend')),
switchMap(() => {
console.log('inside switchMap - start http request');
return httpCall$.pipe(tap(val => console.log('inside switchMap - http response ', val)));
}),
);
现在,我们每次点击都会调用 switchMap ,而不是将单个值传递给 switchMap 函数(作为参考,请查看fromEvent)。fromEvent
用作任何类型输入流的示例。它也可以是Observable
您想要的任何其他输入流。
因此,只要您点击页面上的某个地方,它就会立即记录inside switchMap - click happened
到控制台。之后,switchMap
被调用。这也会inside switchMap - start http request
立即记录。在传递给 的函数末尾switchMap
,我们返回一个httpCall$
Observable。一旦有人订阅它,它就会执行 HTTP 请求。此外,我们再次使用tap来记录 HTTP 响应的值。
<>
我上面已经提到过,switchMap
是传递源的值Observable
并将其传递给需要返回新 的函数Observable
。SwitchMap
将负责订阅返回的 Observable。但现在有两种边缘情况。
- 如果
Observable
返回的内部是发出多个项目的switchMap
长寿命项目,会发生什么情况?Observable
- 如果我的源 Observable 发射速度比内部返回的速度快,会发生什么情况
switchMap
这两个问题都可以通过下面的弹珠图进行可视化。
如果您不熟悉弹珠图的语法,请查看弹珠图部分。
请注意,以下所有大理石图均由Michael Hladky提供。
让我们逐一解决这些问题。如果我们在函数Observable
内部返回一个长生命周期switchMap
对象,我们将在弹珠图上看到,所有通知都会被输出。长生命周期对象的一个非常常见示例Observable
是 HTTP 轮询,我们每 X 秒请求一次 API 并返回其响应。所有这些响应都将传递给链中的下一个操作符Observable
。正如您在 Observable 中所看到的i2$
,两个通知都会传递给output$
Observable。现在我们知道了如何switchMap
处理长生命周期对象Observables
,接下来会出现第二种边缘情况。当源对象的Observable
发射速度快于新创建的 时会发生什么Observable
?一旦有新值从源对象传来,SwitchMap
就会中断执行。如果您使用弹珠图仔细检查这一点,您会注意到,一旦通知到来,流就会立即结束。此外,它将触发新的 Observable并订阅该 Observable。 我们已经说过,它会自动订阅内部的。此外,只要有新值从源对象传来,它就会自动取消订阅。这意味着还包括一个内置的订阅管理机制。Observable
Observable
i1$
b
i2$
switchMap
Observable
Observable
switchMap
您可以在链接的 Stackblitz 示例中体验此操作符。它会在您点击某个位置后触发 HTTP 调用。如果您点击得足够快,您会注意到一些 HTTP 调用被取消了。您可以在 Chrome DevTools 的网络视图中看到这一点。如果其中一个请求带有 标记canceled
,则表示执行该 HTTP 调用的 Observable 已被取消订阅。
现在我们了解了switchMap
,我建议,让我们看看其他操作员。
与类似产品比较
现在是时候兑现我的承诺了。我已经提到过,switchMap
它与concatMap
、mergeMap
&非常相似exhaustMap
。那么它们有什么区别呢?
康卡特地图
让我们从concatMap开始。ConcatMap
它还将源可观察对象的通知传递给内部可观察对象。它订阅该通知并等到它完成后再使用源发出的下一个通知observable
。因此,如果源可观察对象无休止地发射并且比内部可观察对象的完成速度更快,则可能会遇到内存泄漏。ConcatMap
照顾内部可观察对象的实例化的顺序。因此,从业务角度来看,它是我们在本文中介绍的最安全的操作符。理论上,您可能会遇到内存泄漏的技术问题,但是,如果您不确定选择哪个操作符,我建议您采用concatMap
。你或多或少都会没事的。因此,如果您在concatMap
函数内部执行 HTTP 请求,则可以确保在源可观察对象的下一个通知传递给内部可观察对象之前收到响应的响应。同时,它会缓冲这些通知,以便在内部可观察对象完成后立即准备就绪。
concatMap
下面的弹珠图或许可以很好地概括 的行为。它i1$
不再像 那样中断switchMap
,而是等待 的完成i1$
,并在其间缓冲通知,就像 一样b
。第一个流完成后,它将开始处理缓冲的通知。
concatMap
您还可以在我上面链接的 Stackblitz 中探索 的行为。您需要将operators/concatMap.ts
文件中的所有内容复制到index.ts
。代码与之前基本相同,只是现在使用的是concatMap
。如果您在窗口中的某个位置快速点击,您会注意到,它会在您每次点击时发出 HTTP 调用,但只是逐个调用。更重要的是,它会等待每个 HTTP 调用完成后才实例化下一个。
合并地图
MergeMap或flatMap
(只是 的一个别名mergeMap
)与 非常相似concatMap
,尽管它不考虑顺序,并且不会等待一个内部可观察对象完成后再订阅下一个。如果我们继续使用 HTTP 示例,理论上可能会遇到这样的情况:mergeMap 函数内部发起多个 HTTP 请求,如果不等待每个请求完成,则可能会在第一个请求发出响应之前收到第二个实例化的响应。
下图很好地展示了该行为。即使尚未完成,流的通知i2$
也可以传递给流。output$
i1$
您可以再次尝试mergeMap
链接的 Stackblitz 中的行为。
请注意,mergeMap
如果通知的顺序对你来说很重要,那么这绝对是错误的操作符。如果你需要按顺序处理通知,请使用concatMap
!
排气图
最后但同样重要的是exhaustMap。它与 完全相反switchMap
。switchMap 会在收到来自源可观察对象的通知后立即取消订阅内部可观察对象,而 exhaustMap 则会完全忽略这些通知,直到内部可观察对象完成。如果您担心用户点击操作会导致应用程序崩溃,那么 exhaustMap 是完美的选择。但请注意,两者之间的通知不会被缓冲,而是会被完全忽略。这种行为使其与 不同concatMap
,后者会缓冲这些通知。
下面的弹珠图很好地演示了这一点。通知“b”被完全忽略!它会一直等到i1$
完成。之后,它将被来自源 Observable 的下一个通知触发,该通知c
如下方弹珠图所示。
再次,您可以尝试一下 Stackblitz 链接中的行为exhaustMap
。如果您执行双击和三击之类的操作,您会注意到,只要 HTTP 请求正在进行,这些操作就会被忽略。之后,您可以再次单击以重新触发 HTTP 调用。
使用 switchMap 的风险
既然我们已经了解了所有这些出色的运算符,那么你们中的许多人可能已经听说过,使用 可能会出现一些问题switchMap
。让我们来弄清楚这个谣言到底是怎么回事。
的问题在于switchMap
,即使它从客户端角度取消了 HTTP 请求,后端仍然会“接收”这些请求并可能执行某些操作。问题在于,后端无论如何都会处理请求,并且可能会对对象进行修改。这会导致客户端和后端的状态不同。例如,您正在结合执行账户交易switchMap
。如果您多次发起此操作,对于客户端来说,它看起来只发生了一次,但后端会收到多个请求,从而导致多次交易。当然,没有人愿意多次转账:D 只要您不在服务器端对对象实例执行任何修改或触发任何其他操作,就可以使用switchMap
。获取对象或轮询对象是完全有效的使用场景switchMap
,但要注意服务器端的操作或修改!
包起来
- switchMap 订阅每个新通知的内部 Observable
- 它会自动取消订阅旧的内部 Observable
- 使用 switchMap 时要谨慎
- 当你不确定时使用 concatMap
- 考虑长寿命 Observable 的内存泄漏
- 使用 switchMap 进行获取/轮询/只读事务
- 注意服务器端的变化或执行的操作
特别感谢
非常感谢所有帮助我撰写这篇博客文章的人。
感谢Todd Motto、Wes Grimes和Brian Troncone审阅了这篇文章并提供了非常宝贵的反馈。此外,我还要感谢Michael Hladky提供的那些精彩的弹珠图!