如何解决异步请求的竞态问题

3,977 阅读6分钟

如何解决异步请求的竞态问题

疫情期间 大家带好口罩 ^ . ^

免责声明

作者学艺不精又懒的要死,本文如有错误 概不负责 欢迎指出 随缘改正

欢迎来我git做客 github.com/YuArtian/bl…

引子

这大概是所有前端在实际工作中都要解决的问题吧。。。

在现在的交互场景中,搜索框里的实时下拉提示,地图缩放时的数据更新等等。。

只要你多次触发同一个动作 多次调用了同一个接口,你就要考虑时序的问题

这次所讨论的内容并不是给请求加loading的判断啊,或者节流啊防抖啊,或者 async/await啊之类的

因为在下面的场景中这些并不能解决问题。。

请注意,我们的接口响应时间是随机的,而且我们要得到最新的结果,也就是最后一个请求得到的响应

场景描述

现在页面上有一个输入框,随着用户的输入会不断发出异步请求,取回后端返回的结果渲染在页面上。

但是蛋疼的是,接口的响应时间并不确定,也就是说,有可能先请求的后返回,后请求的却先返回了。

如果不作处理,这会导致前端渲染的结果错误(不是最后一个请求返回的结果)

那么。。如何才能保证页面正确的渲染呢?

重现

我们可以先简单重现一下,领会一下精神,代码如下,效果如图

参见链接 abort_0.html

以上

可以看到我们实际想渲染的是第四个请求的结果,但是却被第三的请求的结果后来居上了,导致了显示错误

简单粗暴的解决方式

最简单的想法就是记一下数(其实已经记了)。。。比较一下是不是最后一个就好了

参见链接 abort_1.html

OK,这样看起来已经阔以了

但是这样简单的方法在实际使用中并不方便,你总是需要想办法返回一个计数,然后做比较判断。。每个请求还都要重写一遍。。

鉴于现代的异步请求基本上都会使用 Promise ,接下来我们就介绍一种结合了 PromiseXHR.abort() 的船新方法

XHR.abort() 和 "终止"请求

XMLHttpRequest 提供的 abort 方法 可以用来将 XMLHttpRequestreadyState 置为 0。这样就可以视为请求被 "终止" 了 ^ . ^

但是请注意,这只是前端视角上的"终止",实际上请求还是会到达服务器的(后面我们会证明这一点)。

从 http 原理来讲,也没可能会有所谓的终止请求的。简单设想一下,前端发出一个删除数据的请求,正常的请求流程中,请求被服务器接受,后端操作数据库,删掉数据,然后把结果响应给前端。在这中间前端如果真的能在响应到达之前终止请求的话,那删掉的数据怎么办呢。。。 -,-

所有的终止方法都只能是在到达前端之后不做处理而已,这样在用户看来就是被"终止"了。

真实场景搭建

为了能真实的测试我们的请求和响应的情况,我们就真的写一个随机响应的node服务出来吧 ~

新建一个 app.js , 代码如下

参见链接 app.js

同样的,我们的前端请求也要写一个真实的

现代的异步请求离不开 Promise,即使你用 XHR 也建议用 Promise 包装

实际上,这样的封装只有一次,整个项目都会使用这个封装好的 xhrAdapter

参见链接 abort_2.html

接下来,测试一下是否能重现我们要的场景

可以看到,请求按顺序发出了,但是响应是不定时的。现在舞台搭好了。

Promise 和 XHR.abort()

之前,我们解决问题的思路是给请求计数,通过对比来判断出最后一个请求

现在,换一种思路。在连续的请求过程中,每当我发出一个请求,我就将之前正在 pending 的请求的 Promise reject 掉,并且该请求的 XHR 对象执行 abort()

之前的请求 如果已经有响应的不用管它,我们当前的请求的结果会覆盖它的

这样就能确保最后的响应是正确的了

那么问题来了,怎样才能记录之前的请求,还要能在适当的时机执行对它的一系列操作呢?

这个问题,其实 Promise 自己就是答案。仔细想一想,只要 Promise 的状态改变了,就会在 .then 或者 .catch 中执行我们之前写好的回调函数,而且利用这个回调函数,刚好可以用来保存之前的请求。简直完美

这样的话,就需要我们自己生成一个 Promise 并把它的 .then 回调关联到我们的 xhrAdapter 中,回调函数中会保存当时的 XHR 请求对象和其包装 Promisereject 方法,有了这两个对象就可以达到我们的目的了。

那下面我们就来具体实现一下这个想法

首先,总不能每次都写一遍生成 Promise 的代码。这里就构造一个名为 CancelToken 的类用来生成 Promise,为了防止多次执行取消操作,也对取消请求操作进行记录, 需要同样的构造一个 Cancel 的类

这样的话其实也有一个小问题,就是这个两个类和具体发出请求的方法,也就是 handleInput 肯定是不在一起的了,那如何才能在外部( handleInput 中)控制一个 Promise 的状态呢?

具体的答案我们还是先来看一下代码就知道了

然后,还要对 xhrAdapter 进行改造。为了不具有侵入性,这里读取参数中的 cancelToken 配置,有这个参数的请求才进入控制

参见链接 abort_3.html

实际使用的时候,方法如下

实际效果:

看来效果还是阔以的,而且写法高端了很多

实际上,上面的实现就是 Axios 的源码的简陋版本

Axios 相关文档 Cancellation部分

Axios 源码位置 Cancel部分XHR部分

fetch 和 AbortController

然而并不是所有的请求都是使用 XHR 的,使用 fetch 的并不少见。用了 fetch 的,可以使用 AbortController 来阻止请求。

AbortController abort 来自MDN

abortable-fetch

下面是用 fetchAbortController 结合使用的例子

参见链接:abort_4.html

实际效果:

尾声

至此,我们已经处理了两种前端常见的请求方式 XHRfetch 的竞态问题。当然也只是这种一种场景下的竞态问题,前端会有更多复杂的异步问题需要面对。所以才会有 RxJS 等解决异步问题的库出现。

以后也许会去深入研究更为复杂的异步问题 ^ . ^