阅读 496

图解 JavaScript 竞态处理

前言

最近被公司安排分享一些主题,思来想去,觉得还是想分享关于 JavaScript 竞态相关的知识。于是总结成此文。

这篇博客的目标主要以图例的方式带大家了解 JavaScript 并发与竞态,如有疏漏,欢迎大家指正。

以下正文。

竞态导致的错误

经验较为丰富的开发者,可能会感触于异步代码的确较比同步代码难以理解和编写、维护。“时间是程序里最复杂的因素”。一个现代的 Web 应用无法避免异步的处理。如何更好的意识到异步中的并发与竞态的存在,这是第一步。比如看下图:

假设我们有三个异步请求:A1 -> A2 -> A3,按时间触发,每一个请求经过服务器后,再响应返回,会对应用产生一些副作用 (Effect)。

同时我们假设,每一个请求受网络因素影响,响应返回的时间不定。如上图,实际响应返回顺序是:A3 -> A1 -> A2。因此尽管我们期望的是对应用产生效果的正确请求应该是 A3。最终实际生效的是 A2。于是在实际环境中,最终或许导致一个致命的错误。

那么如何避免这种情况呢?

策略一 —— 去旧迎新

用过 redux-saga 的同学可能知道有个 API 叫做“takeLatest”。rxjs 里也有个操作符叫做“takeLast(1)”。前端可通过状态控制,管理多个请求。

实现思路主要为,每当触发最新的请求时,则取消前置的请求,使得永远只有最新的请求可以最终生效。

备注,取消请求的方法,在原生的 XHR 对象里方法为:XMLHttpRequest.abort()。在 axios 里有 cancelToken 的 API 提供完成。

策略二 —— 控制回调

同样我们也可以任由请求发生,因为我们需要保证的实际上,是 Web 应用最终能以服务器最终的数据产生作用(也即是最新的一个请求所能获取的数据)。

因此,我们实际上或许无需阻止请求的发生,我们控制住请求的前端响应回调执行顺序即可。在此基础上做一个防抖控制,亦可以达到预期中比较良好的体验效果。

策略三 —— 队列

第三种策略,可以将所有发起请求放在前端的一个队列里,逐个发送,在一个请求响应回来后,才发射下一个请求。由于直接将请求从拍成了一条线,也就完全避免了竞态的场景问题发生。

相比于策略一、策略二,这种方法也许看上去是最笨和最慢的一个方法了。但是这个方法是可以在某些场景下,是比前二者更好的选择。

GET or POST 请求场景

以上策略都满足于 GET 请求的场景。

然而,让我们考虑以下场景:

我们的请求不是简单的 GET 请求,而是会对服务器数据库进行操作的 POST 请求,且服务器依赖于 POST 请求操作的执行顺序,从而返回正确的响应。

策略一的弊端在于,即使取消了请求,只是保证了在前端不执行响应回调,但前端实际上也无法控制该请求是否已经到达服务器。也就是说。那实际上服务器数据操作也可能会造成紊乱。

策略二的弊端也同上,由于甚至没有取消请求,如果在同样上述场景下,请求到达服务器的顺序错误,服务器数据库的数据紊乱甚至是必然发生的。

如下图,我们假设服务器数据库 A、B 分别代表用户的两种权限,A+请求会增加数据库 A 值,A-操作会减少数据库 A 值,B 操作同 A,而服务权限不可能为负值,如下图:

因此错误的请求到达顺序导致了数据库数据的错误。

而策略三虽然没有充分利用请求并发的优势,但是通过在前端队列控制发送请求上,就已经完全避免了上述的问题。

关于时序控制

于是基于策略三,亦可以细分为前端的队列控制和后端的时序控制。

前端控制

如图所示,多个请求拍成一个队列,逐个发送,实现后可参见控制台网络面板的瀑布流。

后端控制

有同事提醒我,其实服务器也可以实现这个需求。前端正常发送所有请求,服务器维护多个请求的状态,动态控制请求生效响应顺序。

关于实现

实现方面,我之前已经写过一篇博客。但是主要都是示例代码。参见 关于 JavaScript 并发、竞态场景下的一些思考和解决方案

小结

当然,期望知道别的方法的同学可以不吝指教。

以上,对大家如有助益,不胜荣幸。

关注下面的标签,发现更多相似文章
评论