RxJS: Avoiding switchMap-Related Bugs(避免switchMap 相关的bug)

原文来源: https://blog.angularindepth.com

翻译说明: 中英对照, 意翻, 节选重要部分, 想了解原文的朋友可以去博客地址查找该文。

Let’s use a shopping cart as an example and have a look at an effect — and an epic — that misuses switchMap and then consider some alternative operators.
让我们用购物车作为例子, 看一下 滥用 switchMap的effect 以及 epic , 然后考虑一下一些可以替代的操作符。

Here’s an NgRx effect that misuses switchMap:
这是个 滥用 switchMap 的 NgRx effect:

@Effect()
public removeFromCart = this.actions.pipe(
  ofType(CartActionTypes.RemoveFromCart),
  switchMap(action => this.backend
    .removeFromCarg(action.payload)
    .pipe(
      map(response => new RemoveFromCartFullfilled(response)),
      catchError(error => of(new RemoveFromCartRejected(error)))
    )
  )
  
);

And here’s its equivalent redux-observable epic:
这是和它同样的 redux-observable epic:

const removeFromCart = actions$ => actions$.pipe(
  ofType(actions.REMOVE_FROM_CART),
  switchMap(action => backend
  .removeFromCart(action.payload)
  .pipe(
    map(response => actions.removeFromCartFullfilled(response)),
    catchError(error => of(new RemoveFromCartRejected(error)))
   )
  )
);

Our shopping cart lists the items the user intends to purchase and against each item is a button that removes the item from the cart.
我们的购物车列出了用户要购买的商品, 每件商品都有个删除按钮。

Clicking the button dispatches a RemoveFromCart action to the effect/epic which communicates with the application’s backend and sees the item removed from the cart.
点击该按钮会发送 RemoveFromCart action 到 effect/epic, effect/epic 会和应用程序的后端进行交互, 你会看到商品从购物车中删除。

Most of the time, this will function as intended. However, the use of switchMap has introduced a race condition.
大多数情况下, 这将按预期进行。 然而, 使用 switchMap 会引入竞争条件。

If the user clicks the remove buttons for several items in the cart, what happens depends upon how rapidly the buttons are clicked.
如果用户点击了购物车中几件商品的删除按钮, 会发生什么取决于按钮的点击速度。

If a remove button is clicked when the effect/epic is communicating with the backend — that is, when a removal is pending — the use of switchMap will see the pending removal aborted.
如果在 effect/epic 和后端进行交互时点击了删除按钮——也就是, 当某个删除操作正在等待中时—— 使用switchMap, 会中止等待中的 删除操作。

So, depending upon how rapidly the buttons are clicked, the application might:
因此, 取决于按钮的点击速度, 应用程序可能:

  • remove all of the clicked items from the cart;
    从购物车中删除所有点击的商品

  • remove only some of the clicked items from the cart; or
    从购物车中只删除一部分点击的商品; 或者

  • remove some of the clicked items from the backend’s cart but not reflect their removal from the frontend’s cart.
    从后端的购物车中删除一部分点击的商品, 但是在前端购物车中并没有反映出来。

Clearly, this is a bug.
很明显,这是个bug。

It’s unfortunate that switchMap is often suggested as the first choice when a flattening operator is required, as it’s not safe for all scenarios.
不幸的是,当需要平化操作符时,switchMap 通常被认为是首选,但其对于所有的场景并不都是安全的。

RxJS has four flattening operators to choose from:
RxJS 有4种可以选择的平化操作符:

  • mergeMap (also known as flatMap)
  • concatMap
  • switchMap
  • exhaustMap

Let’s have a look at these operators to see how they differ and to determine the shopping-cart scenarios for which each operator is best suited.
我们看一下这些操作符,看看它们是如何不同的,并确定每个操作符最适合的购物车场景。

mergeMap/flatMap

If switchMap is replaced with mergeMap, the effect/epic will concurrently handle each dispatched action.
如果将 switchMap 替换成 mergeMap, effect/epic 会并行处理每个发出的 action

That is, pending removals will not be aborted; the backend requests will occur concurrently and, when fulfilled, the response actions will be dispatched.
也就是说, 等待中的删除操作不会被中止; 后端请求会同时发生, 当完成后, 响应 actions 会发出。

It’s important to note that due to the concurrent handling of the actions, the order of the responses might not match the order of the requests.
需要注意的是, 由于并行处理actions, 响应的顺序可能和请求的顺序不匹配。

For example, if the user clicks the remove buttons of the first and second items, it’s possible that the removal of the second item might occur before the removal of the first.
举个例子, 如果用户点击了第一件商品和第二件商品的删除按钮, 可能第二件商品的删除要在第一件商品的删除之前发生。

With our cart, the ordering of the removals doesn’t matter, so using mergeMap instead of switchMap fixes the bug.
对于我们的购物车, 删除的顺序并不重要, 所以使用 mergeMap 取代 switchMap 修复了这个bug。

concatMap

The order in which items are removed from the cart might not matter, but there are often actions for which the ordering is important.
从购物车中删除商品的顺序并不重要, 但是也有一些 action 的顺序是重要的。

For example, if our shopping cart has a button for increasing an item’s quantity, it’s important that the dispatched actions are handled in the correct order.
举个例子, 如果我们的购物车有个增加商品数量的按钮, 发出的 action 按顺序处理很重要。

Otherwise, the quantities in the frontend’s cart could end up out-of-sync with the quantities in the backend’s cart.
否则, 前端购物车里的数量最终可能和后端购物车里的数量不同步。

With actions for which the ordering is important, concatMap should be used.
对于 顺序很重要的 action, 应当使用 concatMap

concatMap is the equivalent of using mergeMap with a concurrency of one.
concatMap 相当于 使用 并行数为 1 的 mergeMap

That is, an effect/epic using concatMap will be processing only one backend request at a time — and actions are queued in the order in which they are dispatched.
也就是说, 使用 concatMap 的 effect/epic 每次只处理一个后端请求, 并且 actions 会按发送的顺序排队进行。

concatMap is a safe, conservative choice. If you are unsure of which flattening operator to use in an effect/epic, use concatMap.
concatMap 是安全的、保守的选择。 如果在 effect/epic 中你不知道要用哪个平化操作符, 那么使用concatMap

switchMap

The use of switchMap will see pending backend requests aborted whenever an action of the same type is dispatched.
当发送相同类型的 action 时,使用 switchMap, 将会中止待处理的后端请求。

That makes switchMap unsafe for create, update and delete actions. However, it can also introduce bugs for read actions.
switchMap 用于 创建、 更新 以及删除 action 将是不安全的。 然而, 把它用于 读取 action 同样会引入bug。

Whether or not switchMap is appropriate for a particular read action depends upon whether or not the backend response is still required after another action of the same type is dispatched.
switchMap 用于某个特定的读取 action 是否合适, 取决于另一个同样类型的action发出后,后端响应是否仍然是需要的。

Let’s look at an action with which the use of switchMap would introduce a bug.
我们来看一下使用 switchMap 会引入bug 的一个 action。

If each item in our shopping cart has a details button — for showing some inline details — and the effect/epic that handles the details action uses switchMap, a race condition is introduced.
如果在我们的购物车中每一件商品都有个详情按钮 —— 用于展示一些细节—— 处理 详情 action 的 effect/epic 使用了 switchMap, 引入了竞争条件。

If the user clicks the details buttons of several items, whether or not the details for those items will be displayed depends upon how rapidly the user clicks the buttons.
如果用户多次点击了详情按钮, 这些商品细节的显示与否取决于用户以多快的速度点击按钮。

As with the RemoveFromCart action, using mergeMap would fix the bug.
对于 RemoveFromCart action, 使用 mergeMap 会修复这个bug。

switchMap should only be used in effects/epics for read actions and only when the backend response is not required after another action of the same type is dispatched.
在 effect/epics 中, switchMap 应当只用于 读取 action, 并且只应当用于 当另一个同样类型的action 发出后, 后端响应不再需要的情况。

Let’s look at a scenario in which switchMap would be useful.
我们来看一下switchMap 会非常有用一个场景。

If our application has a GetCart action that sees a request made to the backend for the cart’s contents whenever the user navigates to the cart, it makes sense to use switchMap.
如果我们的应用程序有 GetCart action, 无论何时, 只要用户导航到购物车, 就会发送查询购物车内容的请求到后端。

Then, if the user moves back and forth between the cart and the store, only the most recently dispatched GetCart action will be handled — previous backend requests for the cart’s contents will be aborted.
然后, 如果用户在购物车和商店之间来回变动, 只有最近发出的 GetCart action 才会被处理——之前向后端发出查询购物车内容的请求会被中止。

exhaustMap

exhaustMap is perhaps the least-well-known of the flattening operators, but it’s easily explained: it can be thought of as the opposite of switchMap.
exhaustMap 或许是最不出名的平化操作符了, 但是它很容易解释: 可以把它想成 switchMap 的相反操作。

If switchMap is used, pending backend requests are aborted in favour of more recently dispatched actions.
如果使用了 switchMap, 等待中的后端请求会中止, 最新发出的action 会取而代之。

However, if exhaustMap is used, dispatched actions are ignored whilst there is a pending backend request.
然而, 如果使用了 exhaustMap, 当有后端请求等待时, 发送的 action 会被忽略。

Let’s look at a scenario in which exhaustMap could be used.
我们来看一下 exhaustMap 应当使用的场景。

There’s a particular type of user with whom developers should be familiar: the incessant button clicker.
有种特定的用户类型, 开发者应该很熟悉: 不停点击按钮者。

When the incessant button clicker clicks a button and nothing happens, they click it again. And again. And again.
当用户单击按钮 并且什么也没有发生时, 他们会再次单击按钮, 一次又一次地单击。

If our shopping cart has a refresh button and the effect/epic that handles the refresh uses switchMap, each incessant button click will abort the pending refresh.
如果我们的购物车有个刷新按钮, effect/epic 使用 switchMap 处理刷新, 每次连续的按钮单击会中止等待中的请求

That doesn’t make a whole lot of sense and the incessant button clicker could be clicking for a long, long time before a refresh occurs.
这并没有多大的意义, 用户在刷新发生前可能会单击很多次。

If the effect/epic that handles the refreshing of the cart instead used exhaustMap, a pending refresh request would see the incessant clicks ignored.
如果 effect/epic 使用 exhaustMap 处理购物车的刷新, 等待中的刷新请求会忽略 连续的点击。

TL;DR

To summarise, when you need to use a flattening operator in an effect/epic you should:
总结一下, 当你需要在 effect/epic 中使用平化操作符时, 你应该:

  • use concatMap with actions that should be neither aborted nor ignored and for which the ordering must be preserved — it’s also a conservative choice that will always behave in a predictable manner;
    对 既 不能中止不能忽视 的并且 顺序必须保留 的 actions 使用 concatMap—— 这也同样是一种保守的选择, 其行为总是可预测的。

  • use mergeMap with actions that should be neither aborted nor ignored and for which the ordering is unimportant;
    对 既 不能中止不能忽视 的并且 顺序不重要 的 actions 使用 mergeMap

  • use switchMap with read actions that should be aborted when another action of the same type is dispatched;
    对 当又一个同样类型的 action 发送时 , 应当中止的 actions 使用 switchMap,

  • use exhaustMap with actions that should be ignored whilst an action of the same type is pending.
    使用 exhaustMap, 当同样类型的 action 等待时, 其他actions 应当忽略。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容