Rxjs|Rxjs SwitchMap 的一些容易犯的错误和替代方案

下面是一个在 Effect 里使用 SwitchMap 的例子:从购物车里移除某个行项目

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

购物车列出了用户打算购买的商品,每个商品都有一个从购物车中删除商品的按钮。 单击该按钮会将 RemoveFromCart 操作分派给与应用程序后端通信的对应 API,并查看从购物车中删除的项目。
这段代码看似能够正常运行,但实际上 switchMap 的使用,引入了竞态条件(race condition)。
如果用户单击购物车中多个项目的删除按钮,会出现什么样的行为?
根据客户点击按钮的速度不同,应用程序可能会:
  1. 从购物车中删除所有点击的物品,比如客户点击一个行项目的删除按钮,等删除操作在后台成功执行之后,再点击第二个行项目。
  2. 客户飞快地点击了前两个行项目的删除按钮。第一个行项目的删除请求正在发送往后台服务器的过程当中,则第二个按钮的点击,会取消第一个行项目的删除请求。最后仅仅第二个行项目被删除了。
  3. 客户依次点击了前两个行项目的删除按钮。第一个删除请求已经抵达后台,正在执行后台的删除操作。第二个请求也到达了后台。此时的行为,取决于后台 API 从 cart 上删除行项目时,是否给当前的 cart 加了锁。
我们考虑一下是否能用如下的 Operator 来替代 SwitchMap.
mergeMap/flatMap 如果 switchMap 被 mergeMap 替换,则 effect 的代码将同时处理每个调度的动作。
也就是说,pending 的删除不会被中止;后端请求将同时发生。请求完成时,Effect 会 dispatch 对应的 action.
需要注意的是,由于操作的并发处理,响应的顺序可能与请求的顺序不匹配。 例如,如果用户单击第一个和第二个项目的删除按钮,则第二个项目的删除可能发生在第一个项目的删除之前。
对于购物车里删除行项目的场景而言,删除的顺序并不重要,因此使用 mergeMap 而不是 switchMap 可以修复该错误,规避潜在的竟态条件。
concatMap 从购物车中移除商品的顺序可能无关紧要,但通常有一些操作对排序很重要。
例如,如果我们的购物车有一个增加商品数量的按钮,那么以正确的顺序处理分派的操作很重要。 否则,前端购物车中的数量最终可能与后端购物车中的数量不同步。
对于排序很重要的操作,应使用 concatMap.
concatMap 相当于使用并发为 1 的 mergeMap. 也就是说,使用 concatMap 的 effect 代码一次将只处理一个后端请求,并且操作按照它们被调度的顺序排队。
concatMap 是一个安全而保守的选择。 当不确定在 Effect 中使用 SwitchMap,MergeMap 或者 concatMap 时,使用 concatMap 比较安全。
switchMap 每当调度相同类型的操作时,使用 switchMap 将看到挂起的后端请求中止。这使得 switchMap 对于创建、更新和删除操作不安全。但是,它也可能为读取操作引入错误。
switchMap 是否适用于特定的读取操作取决于在分派另一个相同类型的操作后是否仍需要后端响应。让我们看一下使用 switchMap 会引入错误的操作。
如果我们购物车中的每个商品都有一个详细信息按钮——用于显示一些内联详细信息——并且处理详细信息操作的效果/史诗使用 switchMap,则引入了竞争条件。如果用户点击了几个项目的详细信息按钮,是否显示这些项目的详细信息取决于用户点击按钮的速度。
与 RemoveFromCart 操作一样,使用 mergeMap 可以修复错误。
switchMap 应该只在效果/史诗中用于读取操作,并且仅在分派另一个相同类型的操作后不需要后端响应时使用。
让我们看一下一个实用的 switchMap 使用场景。
如果我们的应用程序的购物车显示商品的总成本加上运费,那么对购物车内容的每次更改之后,都会触发一个 GetCartTotal 的读操作。
将 switchMap 用于处理 GetCartTotal 操作的做法是完全合适的。
如果 Effect 正在处理 GetCartTotal 操作时更改了购物车,则对 pending 请求的响应已经是陈旧的 - 它是更改之前购物车中项目的总数 - 因此中止挂起的读操作请求是合理的。
事实上,中止这个不必要的读请求比允许该读请求完成然后忽略——或者更糟糕的是,在界面上显示陈旧的响应更可取。
exhaustMap exhaustMap 可能是最不为人所知的 flatterning 运算符,但它很容易解释:它可以被认为是 switchMap 的反面。
如果使用 switchMap,pending 的后端请求将被中止,以支持最近一次分发的操作。
反之,如果使用了 exhaustMap,当有一个挂起的后端请求时,分派的动作将被忽略。
开发人员应该熟悉一种特殊类型的用户:倾向于不断重复点击同一个按钮。特别是当不断地点击一个按钮并且没有任何响应时,这些用户会再次点击它。
如果购物车有一个刷新按钮,并且处理刷新的 Effect 代码中使用 switchMap,则每次不断的按钮单击都会中止之前触发的刷新操作。
【Rxjs|Rxjs SwitchMap 的一些容易犯的错误和替代方案】如果处理购物车刷新的 Effect 改为 ExhaustMap,则待处理的刷新请求将反过来忽略不断重复的点击。
总结
  • 将 concatMap 与既不应中止也不应忽略,必须保留其顺序的操作一起使用。使用 concatMap 是一种保守的选择,将始终以可预测的方式运行;
  • 将 mergeMap 与既不应该中止,也不应该忽略,并且先后顺序不重要的动作一起使用;
  • 将 switchMap 与读取操作一起使用,当分派另一个相同类型的操作时,之前的操作应该被中止,这种情况是 switchMap 的最佳适用场合。
  • 如果存在相同类型的操作处于待处理状态时,新触发的相同类型的操作应该被忽略,此时应该是一 exhaustMap.

    推荐阅读