JavaScript 异步竞争态处理
编写 Web 应用的时候,一般来说,我们大多时候处理的都是同步的、线性的业务逻辑。但是正如开篇所说的“时间是程序里最复杂的因素”,应用一旦复杂,往往会遭遇很多异步问题,如果代码中涉及到多个异步请求的时候,这时候就需要慎重考虑了,我们需要意识到的是:
到底我们的异步逻辑是易读的么?可维护的么?哪些是并发场景,哪些是竞态场景,我们有什么对策么?
请求时序问题
一般而言,在前端而言我们经常遇到的异步场景,是请求问题。(当然对应到后端,有可能是各种 IO 操作,比如读写文件、操作数据库等)。
那笔者为何谈到请求,因为大多人都会忽略此类问题。我们往往有时候会发出多个同类型的请求(不一定符合我们意愿),但是每每觉得自己的应用十分健壮,实际上如果没有当心控制“野兽”的话,应用也会相当脆弱!
如下图,应用依照 A1 -> A2 -> A3 顺序发起请求,我们也期望的是 A1 -> A2 -> A3 的顺序返回响应给应用。
但实际上呢。但是每个请求都是十分野性的。我们根本无法把控它哪时候回来!请求的响应顺序极大程度依赖用户的网络环境。比如上图的响应顺序实际上就是 A3 -> A1 -> A2,此时应用将有概率会变得一团糟!
不过也不用担心,实际上,一旦当你注意问题的时候,其实就离解决问题不远了。
那么我们常见的做法会有什么呢?
结束标记
通过应用中的标记状态,在需求请求完成后,标记成功,忽略多余请求,可以巧妙避开请求竞态的陷阱。(对比请求前后的入参值,只拿最新的数据渲染);由于此写法比较常见,不再赘述。
队列化
将请求串行!某些特殊场景下可以使用。在时间线上将多个异步拍平成一条线。野兽请求们依序进入队列(相当于我们给请求们拉起了缰绳,划好了奔跑的道路),如下图:
只有当 A1 请求响应时,才进行 A2 请求,A2 响应成功时,进行 A3 请求。同理以此类推。(注意虽然请求的顺序强行被修改为串行,但并不意味这发起请求的动作也是串行)。因此在从时间维度上大大简化了场景,极大的减少了 bug 的发生概率。
缺点也很明显,请求串行后阻塞了,某些场景下也许做了很多无用功。
取消请求 + 最新
用过 redux-saga 的同学可能知道有个 API 叫做“takeLatest”。rxjs 里也有个操作符叫做“takeLast(1)”。前端可通过状态控制,管理多个请求。
实现思路主要为,每当触发最新的请求时,则取消前置的请求,使得永远只有最新的请求可以最终生效。
备注,取消请求的方法,在原生的 XHR 对象里方法为:XMLHttpRequest.abort()。在 axios 里有 cancelToken 的 API 提供完成。