头图

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,则每次不断的按钮单击都会中止之前触发的刷新操作。

如果处理购物车刷新的 Effect 改为 ExhaustMap,则待处理的刷新请求将反过来忽略不断重复的点击。

总结

  • 将 concatMap 与既不应中止也不应忽略,必须保留其顺序的操作一起使用。使用 concatMap 是一种保守的选择,将始终以可预测的方式运行;
  • 将 mergeMap 与既不应该中止,也不应该忽略,并且先后顺序不重要的动作一起使用;
  • 将 switchMap 与读取操作一起使用,当分派另一个相同类型的操作时,之前的操作应该被中止,这种情况是 switchMap 的最佳适用场合。
  • 如果存在相同类型的操作处于待处理状态时,新触发的相同类型的操作应该被忽略,此时应该是一 exhaustMap.

Jerry Wang的SAP技术专栏
SAP成都研究院开发专家,SAP社区导师,SAP中国技术大使
873 声望
1.6k 粉丝
0 条评论
推荐阅读
SAP OData 开发实战教程:从入门到提高
本教程根据笔者在 SAP 研究院工作多年的实战经验,计划通过 300 篇以上的文章篇幅,深入介绍 SAP 三大主流的 OData 开发技术:

JerryWang_汪子熙阅读 117

封面图
从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木150阅读 12.3k评论 10

正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 7.9k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy46阅读 6k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木66阅读 6.2k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.3k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木44阅读 7.4k评论 6

873 声望
1.6k 粉丝
宣传栏