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