我们开发web页面时候,也许会遇到和异步请求取消相关的问题。
如:在一个请求发送之后,用户做了一个取消指令,为了节省资源,我们需要把已经被用户取消的请求终止掉;或者是一个页面正在用ajax请求后台,突然页面发生了跳转,而我们未完成的ajax莫名其妙地走进了error里面了。
为了解决这两问题,我们今天一起看看和异步请求取消相关的那些事。
1.ajax的取消
当我们创建一个XMLHttpRequest对象的时候,我们就会发现两个api——abort和onabort,这就是终止异步请求的方法与其响应事件。
执行完abort之后,浏览器和被请求的服务器都会发生什么呢?MDN的解释非常的简单,就是中断已发送的请求。这个请求指的是http请求,而不是tcp连接,这样就会出现一个问题,基于http请求原理,当一个请求从客户端发出去之后,服务器端收到请求后,一个请求过程就结束了,这时就算是客户端abort这个请求,服务器端仍会做出完整的响应,只是这个响应客户端不会接收罢了。
所以这个abort是仅给客户端使用的,不能作为供服务器端判断请求是否继续执行的依据。
那么被abort的请求对客户端有哪些影响呢?我们可以做一个实验。
var xhr = new XMLHttpRequest(); xhr.open("GET","#"); xhr.send(); xhr.onload = function(){ console.log("abort前"); console.log(xhr.readyState); console.log(xhr.status); xhr.abort(); console.log("abort后"); console.log(xhr.readyState); console.log(xhr.status); }
我们可以看到readyState和status在abort之后被重置回0。
那么我们能用这两个参数作为判断请求被abort的依据吗?首先能够让status等于0的情况太多了,如请求本地资源、网络不可用、请求超时,这些都可以让status被置0;readyState等于0能否作为请求是否被abort了还不好说,需要进一步判断,readyState等于0相当于请求未初始化,请求都已经send了readyState却等于0,笔者认为是可以作为abort的判断依据的,但是无法完全证明。
有没有更可靠的证明请求是否执行了abort方法呢?有,答案是使用onabort,onabort作为abort的响应函数,是最直接有效的判断abort手段。
2.页面跳转时候ajax会自动“abort”
笔者从前认为abort离我很远,但是在实际项目中,笔者发现页面我开发的请求经常被abort。这个abort动作当然不是我发起的,也不是用户发起的,他是浏览器自动发起的。笔者发现一个页面跳转的时候,浏览器会自动把所有响应未完成的请求执行“abort”,而响应已完成的请求则不会这样。我们可以做一个实验
//要在chrome或者webkit内核上运行 var xhr = new XMLHttpRequest(); //访问一个不存在的地址 取保请求不会马上响应 xhr.open("GET","http://aaa.bbbbbbbb.com"); xhr.send(); xhr.onabort = function(){ console.log(xhr.readyState); console.log(xhr.status); alert("执行onabort"); }; setTimeout(function(){ //模拟跳转页面 location.href = "http://www.baidu.com" },0);
结果网页上弹出了一个alert,显示着"执行onabort"。
再看控制台,我们会发现status不变还是0,而readyState却是4,这也是浏览器发出的abort和手动执行abort最大不同。
以上测试仅在chrome上有效,ie、edge、火狐在页面跳转的时候,不会触发未完成的请求的onabort事件,但是会触发onreadystatechange事件。不管怎么讲,当页面发生跳转的时候,浏览器可能会“abort”我们的异步请求。
3.jquery对abort的处理
jquery又是如何对abort封装的呢?我们在使用$.ajax(包括众多用$.ajax封装的方法,如$.get、$.post)的时候,会返回一个xhr对象,这个基于$.deferred.promise封装的jquery自己的对象,而不是原始的XMLHttpRequest或者ie的ActiveXObject对象。在这个对象中定义了如abort等方法,使得开发者可以手动abort一个ajax请求。
var xhr = $.ajax(url); xhr.abort();
另外,jquery的超时也是通过setTimeout和abort实现的,所以当你使用jquery发出的请求超时的时候,实际上是被jquery把请求abort了。如何区分jquery的超时和手动abort呢?方法就是靠stutusText,对于timeout和abort两个客户端做出的响应,jquery会给stutusText设定固定的值,abort的时候,stutusText的值为“abort”,超时的时候stutusText值是“timeout”。
4.jquery与页面跳转的ajax“abort”
如果仅仅是页面跳转的时候,chrome浏览器会自动执行未完成的请求的abort方法,那笔者也不会专门写一个章节去分析这个过程。因为笔者发现jquery的$.ajax这个方法中,未完成的ajax在页面跳转的时候,也会触发error事件,而且你区分不出来是浏览器取消还是请求真的发生了error。
大家可以运行如下代码,在ie和chrome两大浏览器下都会弹出的alert对话框。
var xhr = $.ajax({ type:"get", url:"http://aaa.bbbbbbbb.com", error:function(){ console.log(arguments); console.log(xhr.readyState); console.log(xhr.status); alert("执行onerror"); } }); setTimeout(function(){ //模拟跳转页面 location.href = "http://www.baidu.com" },100);
页面一跳转就进error,而且status和readyState都是0,stutusText仅仅显示一个“error”,jquery真是让人佩(蛋)服(疼)的五体投地-_-||。
为什么会这样呢?那么jquery又是如何处理onabort的呢?
笔者发现jquery1.x和2.x都会触发这个现象,所以分别参考jquery1.x和2.x的源码讨论。
jquery1.x其实并没有监听onabort事件,而是统一监听onreadystatechange(具体可以参考github的xhr.js和ajax.js源码),根据status是否是成功响应的http状态码,来确定执行error还是success方法。同时,jquery也没有获取浏览器的readyState的值,而是通过status是否为0去计算自己的xhr.readyState,可以说所有的响应全是靠status一个变量决定的,这就导致了我们无法区分浏览器取消事件还是真正的错误的问题。
在jquery2.x中,不再仅监听onreadystatechange(具体可以参考github的xhr.js和ajax.js源码),而是对onload、onerror、onabort(不支持onabort事件的ie9还是监听的onreadystatechange事件)全面监听,并由这些响应事件的结果去确定究竟执行error还是success。这个处理看似更合理了,然而却并没有什么卵用,因为没有监听chrome浏览器的readyState实际值,仍然是通过status去计算readyState,所以仍然会触发error事件,而且xhr.readyState的值还是0,stutusText还是仅仅显示一个“error”。
说白了这就是jquery的一个bug,仅仅是根据status是否为0去判断ajax结果,同时不返回浏览器真正的readyState值;当然我们也可以说是浏览器的bug,为什么chrome浏览器在页面跳转的时候要abort的请求呢。
不管怎么样,笔者建议在使用jquery的$ajax做异步请求的时候,千万不要在error回调中使用系统的模态框(如alert、confirm等),否则用户在使用你的页面的时候经常会出现意想不到的弹框。
5.fetch及promise如何取消与取消处理的
fetch作为ajax的升级版,越来越多的浏览器已经支持他了,那fetch又是如何取消异步请求的呢?答案是fetch暂时不能被取消...,因为没有对应的api。
虽然不能取消,但是还是有替代品,当然这只是自欺欺人的做法,因为fetch根本没有被真正取消,他的资源也没有被释放。
标题和fetch挂钩,这让笔者感觉有点大,因为笔者现在的项目中还不准备使用fetch。实际上笔者更想聊一聊我对abort和promise的看法,因为不管我们用不用fetch,将异步请求封装成promise供后续处理都是我们现在开发的主流做法,那么如何用promise做abrot呢?
promise仅有两个完成态,resolved和rejected。一个可以当做success处理,另一个可以当做error处理。那我们的abort的结果应该算在哪个里面呢?abort肯定不能将其看成success,但是abort是我们主动的动作(也可能是浏览器发出的被动abort),并不是发生真的发生了错误,将其列入error看起来也不合适。
其实所有非预想的结果都是异常,所以abort当然也是异常,既然是异常就应该当rejected对待。只是resolved的处理方案因为结果是预期中的,所有处理起来比较容易,但是rejected的处理往往很困难,因为各种异常的处理方法是应该不一样的。比如abort这种异常,如果是因为用户主动操作而产生的异常,那这种异常是不应该提示给用户的,所以abort引起的异常应该包装为特定的异常再进行rejected处理,以便在catch中,可以知道是什么异常,并作出对应的处理。
最后一点就是promise里resolved和rejected是不能切换的,所以一旦一个请求得到了响应,就不能再被abort了,而XMLHttpRequest对象是可以随时执行abort的,这一点也是使用promise封装异步请求和直接使用XMLHttpRequest的一个不同点。