前言
最近被公司安排分享一些主题,思来想去,觉得还是想分享关于 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 并发、竞态场景下的一些思考和解决方案
小结
当然,期望知道别的方法的同学可以不吝指教。
以上,对大家如有助益,不胜荣幸。