<!DOCTYPE html>
<html>
<body>
<script>
function reqListener () {
console.log(this);
console.log(this.getAllResponseHeaders());
console.log(this.responseText);
}
var params = "lorem=ipsum&name=binny";
var oReq = new XMLHttpRequest();
oReq.upload.onloadstart = function(){
// alert('onloadstart');
};
oReq.upload.onprogress = function(){
alert('onprogress');
};
oReq.onprogress = function(){
alert('download onprogress');
}
oReq.onload = reqListener;
oReq.onreadystatechange = function(){
alert(this.readyState);
}
oReq.onerror = function(){
alert('error');
}
oReq.open("POST", "http://localhost:81/DeviceRegisters/ajax", true);
oReq.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

oReq.send(params);
</script>
</body>
</html>


  

------------------------------------------------------------------------------

什么是readyState

  readyState是XMLHttpRequest对象的一个属性,用来标识当前XMLHttpRequest对象处于什么状态。

  readyState总共有5个状态值,分别为0~4,每个值代表了不同的含义,如下表所示:

  0  

 未初始化状态:此时,已经创建了一个XMLHttpRequest对象

  1

 准备发送状态:此时,已经调用了XMLHttpRequest对象的open方法,并且XMLHttpRequest对象已经准备好将一个请求发送到服务器端

  2

 已经发送状态:此时,已经通过send方法把一个请求发送到服务器端,但是还没有收到一个响应

  3

 正在接收状态:此时,已经接收到HTTP响应头部信息,但是消息体部分还没有完全接收到

  4

 完成响应状态:此时,已经完成了HTTP响应的接收

什么是status

  status是XMLHttpRequest对象的一个属性,表示响应的HTTP状态码。

  在HTTP1.1协议下,HTTP状态码总共可分为5大类,如下表所示:

  1XX  

 服务器收到请求,需要继续处理。例如101状态码,表示服务器将通知客户端使用更高版本的HTTP协议。

  2XX  

 请求成功。例如200状态码,表示请求所希望的响应头或数据体将随此响应返回。

  3XX  

 重定向。例如302状态码,表示临时重定向,请求将包含一个新的URL地址,客户端将对新的地址进行GET请求。

  4XX  

 客户端错误。例如404状态码,表示客户端请求的资源不存在。

  5XX  

 服务器错误。例如500状态码,表示服务器遇到了一个未曾预料的情况,导致了它无法完成响应,一般来说,这个问题会在程序代码出错时出现。

---------------------------------

看到标题时,有些同学可能会想:“我已经用​​xhr​​成功地发过很多个​​Ajax​​请求了,对它的基本操作已经算挺熟练了。” 我之前的想法和你们一样,直到最近我使用​​xhr​​时踩了不少坑儿,我才突然发现其实自己并不够了解​​xhr​​,我知道的只是最最基本的使用。
于是我决定好好地研究一番​​xhr​​的真面目,可拜读了不少博客后都不甚满意,于是我决定认真阅读一遍W3C的​​XMLHttpRequest​​标准。看完标准后我如同醍醐灌顶一般,感觉到了从未有过的清澈。这篇文章就是参考W3C的​​XMLHttpRequest​​标准和结合一些实践验证总结而来的。

​Ajax​​和​​XMLHttpRequest​

我们通常将​​Ajax​​等同于​​XMLHttpRequest​​,但细究起来它们两个是属于不同维度的2个概念。

以下是我认为对​​Ajax​​较为准确的解释:(摘自​​what is Ajax​​)
AJAX stands for Asynchronous JavaScript and XML. AJAX is a new technique for creating better, faster, and more interactive web applications with the help of XML, HTML, CSS, and Java Script.

AJAX is based on the following open standards:

  • Browser-based presentation using HTML and Cascading Style Sheets (CSS).
  • Data is stored in XML format and fetched from the server.
  • Behind-the-scenes data fetches using XMLHttpRequest objects in the browser.
  • JavaScript to make everything happen.

从上面的解释中可以知道:​​ajax​​是一种技术方案,但并不是一种新技术。它依赖的是现有的​​CSS​​/​​HTML​​/​​Javascript​​,而其中最核心的依赖是浏览器提供的​​XMLHttpRequest​​对象,是这个对象使得浏览器可以发出​​HTTP​​请求与接收​​HTTP​​响应。

所以我用一句话来总结两者的关系:我们使用​​XMLHttpRequest​​对象来发送一个​​Ajax​​请求。

​XMLHttpRequest​​的发展历程

​XMLHttpRequest​​一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了​​XMLHttpRequest标准​​。​​XMLHttpRequest​​标准又分为​​Level 1​​和​​Level 2​​。

​XMLHttpRequest Level 1​​主要存在以下缺点:

  • 受同源策略的限制,不能发送跨域请求;
  • 不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;
  • 在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;

那么​​Level 2​​对​​Level 1​​ 进行了改进,​​XMLHttpRequest Level 2​​中新增了以下功能:

  • 可以发送跨域请求,在服务端允许的情况下;
  • 支持发送和接收二进制数据;
  • 新增formData对象,支持发送表单数据;
  • 发送和获取数据时,可以获取进度信息;
  • 可以设置请求的超时时间;

当然更详细的对比介绍,可以参考​​阮老师的这篇文章​​,文章中对新增的功能都有具体代码示例。

​XMLHttpRequest​​兼容性

关于​​xhr​​的浏览器兼容性,大家可以直接查看“Can I use”这个网站提供的结果​​XMLHttpRequest兼容性​​,下面提供一个截图。


从图中可以看到:

  • IE8/IE9、Opera Mini 完全不支持xhr对象
  • IE10/IE11部分支持,不支持 xhr.responseTypejson
  • 部分浏览器不支持设置请求超时,即无法使用xhr.timeout
  • 部分浏览器不支持xhr.responseTypeblob

细说​​XMLHttpRequest​​如何使用

先来看一段使用​​XMLHttpRequest​​发送​​Ajax​​请求的简单示例代码。

function sendAjax() {
//构造表单数据
var formData = new FormData();
formData.append('username', 'johndoe');
formData.append('id', 123456);
//创建xhr对象
var xhr = new XMLHttpRequest();
//设置xhr请求的超时时间
xhr.timeout = 3000;
//设置响应返回的数据格式
xhr.responseType = "text";
//创建一个 post 请求,采用异步
xhr.open('POST', '/server', true);
//注册相关事件回调处理函数
xhr.onload = function(e) {
if(this.status == 200||this.status == 304){
alert(this.responseText);
}
};
xhr.ontimeout = function(e) { ... };
xhr.onerror = function(e) { ... };
xhr.upload.onprogress = function(e) { ... };

//发送数据
xhr.send(formData);
}


上面是一个使用​​xhr​​发送表单数据的示例,整个流程可以参考注释。



接下来我将站在使用者的角度,以问题的形式介绍​​xhr​​的基本使用。
我对每一个问题涉及到的知识点都会进行比较细致地介绍,有些知识点可能是你平时忽略关注的。

如何设置request header

在发送​​Ajax​​请求(实质是一个​​HTTP​​请求)时,我们可能需要设置一些请求头部信息,比如​​content-type​​、​​connection​​、​​cookie​​、​​accept-xxx​​等。​​xhr​​提供了​​setRequestHeader​​来允许我们修改请求 header。

​void setRequestHeader(DOMString header, DOMString value);​

注意点:

  • 方法的第一个参数 header 大小写不敏感,即可以写成content-type,也可以写成Content-Type,甚至写成content-Type;
  • Content-Type的默认值与具体发送的数据类型有关,请参考本文【可以发送什么类型的数据】一节;
  • setRequestHeader必须在open()方法之后,send()方法之前调用,否则会抛错;
  • setRequestHeader可以调用多次,最终的值不会采用覆盖override的方式,而是采用追加append的方式。下面是一个示例代码:
var client = new XMLHttpRequest();
client.open('GET', 'demo.cgi');
client.setRequestHeader('X-Test', 'one');
client.setRequestHeader('X-Test', 'two');
// 最终request header中"X-Test"为: one, two
client.send();


如何获取response header

​xhr​​提供了2个用来获取响应头部的方法:​​getAllResponseHeaders​​和​​getResponseHeader​​。前者是获取 response 中的所有header 字段,后者只是获取某个指定 header 字段的值。另外,​​getResponseHeader(header)​​的​​header​​参数不区分大小写。

​DOMString getAllResponseHeaders();​DOMString getResponseHeader(DOMString header);

这2个方法看起来简单,但却处处是坑儿。

你是否遇到过下面的坑儿?——反正我是遇到了。。。

  1. 使用getAllResponseHeaders()看到的所有response header与实际在控制台 Network 中看到的 response header 不一样
  2. 使用getResponseHeader()获取某个 header 的值时,浏览器抛错Refused to get unsafe header "XXX"

经过一番寻找最终在 ​​Stack Overflow找到了答案​​。

  • 原因1:W3C的 xhr 标准中做了限制,规定客户端无法获取 response 中的 Set-CookieSet-Cookie2这2个字段,无论是同域还是跨域请求;
  • 原因2:W3C 的 cors 标准对于跨域请求也做了限制,规定对于跨域请求,客户端允许获取的response header字段只限于“simple response header”和“Access-Control-Expose-Headers” (两个名词的解释见下方)。

"​​simple response header​​"包括的 header 字段有:​​Cache-Control​​,​​Content-Language​​,​​Content-Type​​,​​Expires​​,​​Last-Modified​​,​​Pragma​​;
"​​Access-Control-Expose-Headers​​":首先得注意是"​​Access-Control-Expose-Headers​​"进行跨域请求时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。

所以​​getAllResponseHeaders()​​只能拿到限制以外(即被视为​​safe​​)的header字段,而不是全部字段;而调用​​getResponseHeader(header)​​方法时,​​header​​参数必须是限制以外的header字段,否则调用就会报​​Refused to get unsafe header​​的错误。

如何指定​​xhr.response​​的数据类型

有些时候我们希望​​xhr.response​​返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过​​xhr.response​​拿到的直接就是一个 js 对象,我们该怎么实现呢?

有2种方法可以实现,一个是​​level 1​​就提供的​​overrideMimeType()​​方法,另一个是​​level 2​​才提供的​​xhr.responseType​​属性。

​xhr.overrideMimeType()​

​overrideMimeType​​是​​xhr level 1​​就有的方法,所以浏览器兼容性良好。这个方法的作用就是用来重写​​response​​的​​content-type​​,这样做有什么意义呢?比如:server 端给客户端返回了一份​​document​​或者是 ​​xml​​文档,我们希望最终通过​​xhr.response​​拿到的就是一个​​DOM​​对象,那么就可以用​​xhr.overrideMimeType('text/xml; charset = utf-8')​​来实现。

再举一个使用场景,我们都知道​​xhr level 1​​不支持直接传输blob二进制数据,那如果真要传输 blob 该怎么办呢?当时就是利用​​overrideMimeType​​方法来解决这个问题的。

下面是一个获取图片文件的代码示例:

var xhr = new XMLHttpRequest();
//向 server 端获取一张图片
xhr.open('GET', '/path/to/image.png', true);

// 这行是关键!
//将响应数据按照纯文本格式来解析,字符集替换为用户自己定义的字符集
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function(e) {
if (this.readyState == 4 && this.status == 200) {
//通过 responseText 来获取图片文件对应的二进制字符串
var binStr = this.responseText;
//然后自己再想方法将逐个字节还原为二进制数据
for (var i = 0, len = binStr.length; i < len; ++i) {
var c = binStr.charCodeAt(i);
//String.fromCharCode(c & 0xff);
var byte = c & 0xff;
}
}
};

xhr.send();


代码示例中​​xhr​​请求的是一张图片,通过将 ​​response​​ 的 ​​content-type​​ 改为'text/plain; charset=x-user-defined',使得 ​​xhr​​ 以纯文本格式来解析接收到的blob 数据,最终用户通过​​this.responseText​​拿到的就是图片文件对应的二进制字符串,最后再将其转换为 blob 数据。

​xhr.responseType​

​responseType​​是​​xhr level 2​​新增的属性,用来指定​​xhr.response​​的数据类型,目前还存在些兼容性问题,可以参考本文的【​​XMLHttpRequest​​的兼容性】这一小节。那么​​responseType​​可以设置为哪些格式呢,我简单做了一个表,如下:

​xhr.response​​ 数据类型

说明

​""​

​String​​字符串

默认值(在不设置​​responseType​​时)

​"text"​

​String​​字符串


​"document"​

​Document​​对象

希望返回 ​​XML​​ 格式数据时使用

​"json"​

​javascript​​ 对象

存在兼容性问题,IE10/IE11不支持

​"blob"​

​Blob​​对象


​"arrayBuffer"​

​ArrayBuffer​​对象


下面是同样是获取一张图片的代码示例,相比​​xhr.overrideMimeType​​,用​​xhr.response​​来实现简单得多。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
//可以将`xhr.responseType`设置为`"blob"`也可以设置为`" arrayBuffer"`
//xhr.responseType = 'arrayBuffer';
xhr.responseType = 'blob';

xhr.onload = function(e) {
if (this.status == 200) {
var blob = this.response;
...
}
};

xhr.send();


小结

虽然在​​xhr level 2​​中,2者是共同存在的。但其实不难发现,​​xhr.responseType​​就是用来取代​​xhr.overrideMimeType()​​的,​​xhr.responseType​​功能强大的多,​​xhr.overrideMimeType()​​能做到的​​xhr.responseType​​都能做到。所以我们现在完全可以摒弃使用​​xhr.overrideMimeType()​​了。

如何获取response数据

​xhr​​提供了3个属性来获取请求返回的数据,分别是:​​xhr.response​​、​​xhr.responseText​​、​​xhr.responseXML​

  • xhr.response
  • 默认值:空字符串""
  • 当请求完成时,此属性才有正确的值
  • 请求未完成时,此属性的值可能是""或者 null,具体与 xhr.responseType有关:当responseType"""text"时,值为""responseType为其他值时,值为 null
  • xhr.responseText
  • 默认值为空字符串""
  • 只有当 responseType 为"text"""时,xhr对象上才有此属性,此时才能调用xhr.responseText,否则抛错
  • 只有当请求成功时,才能拿到正确值。以下2种情况下值都为空字符串"":请求未完成、请求失败
  • xhr.responseXML
  • 默认值为 null
  • 只有当 responseType 为"text""""document"时,xhr对象上才有此属性,此时才能调用xhr.responseXML,否则抛错
  • 只有当请求成功且返回数据被正确解析时,才能拿到正确值。以下3种情况下值都为null:请求未完成、请求失败、请求成功但返回数据无法被正确解析时

如何追踪​​ajax​​请求的当前状态

在发一个​​ajax​​请求后,如果想追踪请求当前处于哪种状态,该怎么做呢?

用​​xhr.readyState​​这个属性即可追踪到。这个属性是只读属性,总共有5种可能值,分别对应​​xhr​​不同的不同阶段。每次​​xhr.readyState​​的值发生变化时,都会触发​​xhr.onreadystatechange​​事件,我们可以在这个事件中进行相关状态判断。

  xhr.onreadystatechange = function () {
switch(xhr.readyState){
case 1://OPENED
//do something
break;
case 2://HEADERS_RECEIVED
//do something
break;
case 3://LOADING
//do something
break;
case 4://DONE
//do something
break;
}


状态

描述

​0​

​UNSENT​​ (初始状态,未打开)

此时​​xhr​​对象被成功构造,​​open()​​方法还未被调用

​1​

​OPENED​​ (已打开,未发送)

​open()​​方法已被成功调用,​​send()​​方法还未被调用。注意:只有​​xhr​​处于​​OPENED​​状态,才能调用​​xhr.setRequestHeader()​​和​​xhr.send()​​,否则会报错

​2​

​HEADERS_RECEIVED​​(已获取响应头)

​send()​​方法已经被调用, 响应头和响应状态已经返回

​3​

​LOADING​​ (正在下载响应体)

响应体(​​response entity body​​)正在下载中,此状态下通过​​xhr.response​​可能已经有了响应数据

​4​

​DONE​​ (整个数据传输过程结束)

整个数据传输过程结束,不管本次请求是成功还是失败

如何设置请求的超时时间

如果请求过了很久还没有成功,为了不会白白占用的网络资源,我们一般会主动终止请求。​​XMLHttpRequest​​提供了​​timeout​​属性来允许设置请求的超时时间。

​xhr.timeout​

单位:milliseconds 毫秒

默认值:​​0​​,即不设置超时

很多同学都知道:从请求开始 算起,若超过 ​​timeout​​ 时间请求还没有结束(包括成功/失败),则会触发ontimeout事件,主动结束该请求。

【那么到底什么时候才算是请求开始 ?】

——​​xhr.onloadstart​​事件触发的时候,也就是你调用​​xhr.send()​​方法的时候。

因为​​xhr.open()​​只是创建了一个连接,但并没有真正开始数据的传输,而​​xhr.send()​​才是真正开始了数据的传输过程。只有调用了​​xhr.send()​​,才会触发​​xhr.onloadstart​​ 。

【那么什么时候才算是请求结束 ?】

—— ​​xhr.loadend​​事件触发的时候。

另外,还有2个需要注意的坑儿:

  1. 可以在 send()之后再设置此xhr.timeout,但计时起始点仍为调用xhr.send()方法的时刻。
  2. xhr为一个sync同步请求时,xhr.timeout必须置为0,否则会抛错。原因可以参考本文的【如何发一个同步请求】一节。

如何发一个同步请求

​xhr​​默认发的是异步请求,但也支持发同步请求(当然实际开发中应该尽量避免使用)。到底是异步还是同步请求,由​​xhr.open()​​传入的​​async​​参数决定。

​open(method, url [, async = true [, username = null [, password = null]]])​

  • method: 请求的方式,如GET/POST/HEADER等,这个参数不区分大小写
  • url: 请求的地址,可以是相对地址如example.php,这个相对是相对于当前网页的url路径;也可以是绝对地址如http://www.example.com/example.php
  • async: 默认值为true,即为异步请求,若async=false,则为同步请求

在我认真研读W3C 的 xhr 标准前,我总以为同步请求和异步请求只是阻塞和非阻塞的区别,其他什么事件触发、参数设置应该是一样的,事实证明我错了。

W3C 的 xhr标准中关于​​open()​​方法有这样一段说明:

Throws an "InvalidAccessError" exception if async is false, the JavaScript global environment is a document environment, and either the timeout attribute is not zero, the withCredentials attribute is true, or the responseType attribute is not the empty string.

从上面一段说明可以知道,当​​xhr​​为同步请求时,有如下限制:

  • xhr.timeout必须为0
  • xhr.withCredentials必须为 false
  • xhr.responseType必须为""(注意置为"text"也不允许)

若上面任何一个限制不满足,都会抛错,而对于异步请求,则没有这些参数设置上的限制。

之前说过页面中应该尽量避免使用​​sync​​同步请求,为什么呢?

因为我们无法设置请求超时时间(​​xhr.timeout​​为​​0​​,即不限时)。在不限制超时的情况下,有可能同步请求一直处于​​pending​​状态,服务端迟迟不返回响应,这样整个页面就会一直阻塞,无法响应用户的其他交互。

另外,标准中并没有提及同步请求时事件触发的限制,但实际开发中我确实遇到过部分应该触发的事件并没有触发的现象。如在 chrome中,当​​xhr​​为同步请求时,在​​xhr.readyState​​由​​2​​变成​​3​​时,并不会触发 ​​onreadystatechange​​事件,​​xhr.upload.onprogress​​和 ​​xhr.onprogress​​事件也不会触发。

如何获取上传、下载的进度

在上传或者下载比较大的文件时,实时显示当前的上传、下载进度是很普遍的产品需求。

我们可以通过​​onprogress​​事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的​​onprogress​​事件:

  • 上传触发的是xhr.upload对象的 onprogress事件
  • 下载触发的是xhr对象的onprogress事件
xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
if (event.lengthComputable) {
var completedPercent = event.loaded / event.total;
}
}


可以发送什么类型的数据

void send(data);

​xhr.send(data)​​的参数data可以是以下几种类型:

  • ArrayBuffer
  • Blob
  • Document
  • DOMString
  • FormData
  • null

如果是 GET/HEAD请求,​​send()​​方法一般不传参或传 ​​null​​。不过即使你真传入了参数,参数也最终被忽略,​​xhr.send(data)​​中的data会被置为 ​​null​​.

​xhr.send(data)​​中data参数的数据类型会影响请求头部​​content-type​​的默认值:

  • 如果data是 Document 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8
  • 如果data是 DOMString 类型,content-type默认值为text/plain;charset=UTF-8
  • 如果data是 FormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]
  • 如果data是其他类型,则不会设置content-type的默认值

当然这些只是​​content-type​​的默认值,但如果用​​xhr.setRequestHeader()​​手动设置了中​​content-type​​的值,以上默认值就会被覆盖。

另外需要注意的是,若在断网状态下调用​​xhr.send(data)​​方法,则会抛错:​​Uncaught NetworkError: Failed to execute 'send' on 'XMLHttpRequest'​​。一旦程序抛出错误,如果不 catch 就无法继续执行后面的代码,所以调用 ​​xhr.send(data)​​方法时,应该用 ​​try-catch​​捕捉错误。

try{
xhr.send(data)
}catch(e) {
//doSomething...
};


​xhr.withCredentials​​与 ​​CORS​​ 什么关系

我们都知道,在发同域请求时,浏览器会将​​cookie​​自动加在​​request header​​中。但大家是否遇到过这样的场景:在发送跨域请求时,​​cookie​​并没有自动加在​​request header​​中。

造成这个问题的原因是:在​​CORS​​标准中做了规定,默认情况下,浏览器在发送跨域请求时,不能发送任何认证信息(​​credentials​​)如"​​cookies​​"和"​​HTTP authentication schemes​​"。除非​​xhr.withCredentials​​为​​true​​(​​xhr​​对象有一个属性叫​​withCredentials​​,默认值为​​false​​)。

所以根本原因是​​cookies​​也是一种认证信息,在跨域请求中,​​client​​端必须手动设置​​xhr.withCredentials=true​​,且​​server​​端也必须允许​​request​​能携带认证信息(即​​response header​​中包含​​Access-Control-Allow-Credentials:true​​),这样浏览器才会自动将​​cookie​​加在​​request header​​中。

另外,要特别注意一点,一旦跨域​​request​​能够携带认证信息,​​server​​端一定不能将​​Access-Control-Allow-Origin​​设置为​​*​​,而必须设置为请求页面的域名。

​xhr​​相关事件

事件分类

​xhr​​相关事件有很多,有时记起来还挺容易混乱。但当我了解了具体代码实现后,就容易理清楚了。下面是​​XMLHttpRequest​​的部分实现代码:

interface XMLHttpRequestEventTarget : EventTarget {
// event handlers
attribute EventHandler onloadstart;
attribute EventHandler onprogress;
attribute EventHandler onabort;
attribute EventHandler onerror;
attribute EventHandler onload;
attribute EventHandler ontimeout;
attribute EventHandler onloadend;
};

interface XMLHttpRequestUpload : XMLHttpRequestEventTarget {

};

interface XMLHttpRequest : XMLHttpRequestEventTarget {
// event handler
attribute EventHandler onreadystatechange;
readonly attribute XMLHttpRequestUpload upload;
};


从代码中我们可以看出:

  1. XMLHttpRequestEventTarget接口定义了7个事件:
  • onloadstart
  • onprogress
  • onabort
  • ontimeout
  • onerror
  • onload
  • onloadend
  1. 每一个XMLHttpRequest里面都有一个upload属性,而upload是一个XMLHttpRequestUpload对象
  2. XMLHttpRequestXMLHttpRequestUpload都继承了同一个XMLHttpRequestEventTarget接口,所以xhrxhr.upload都有第一条列举的7个事件
  3. onreadystatechangeXMLHttpRequest独有的事件

所以这么一看就很清晰了:

​xhr​​一共有8个相关事件:7个​​XMLHttpRequestEventTarget​​事件+1个独有的​​onreadystatechange​​事件;而​​xhr.upload​​只有7个​​XMLHttpRequestEventTarget​​事件。

事件触发条件

下面是我自己整理的一张​​xhr​​相关事件触发条件表,其中最需要注意的是 ​​onerror​​ 事件的触发条件。

事件

触发条件

​onreadystatechange​

每当​​xhr.readyState​​改变时触发;但​​xhr.readyState​​由非​​0​​值变为​​0​​时不触发。

​onloadstart​

调用​​xhr.send()​​方法后立即触发,若​​xhr.send()​​未被调用则不会触发此事件。

​onprogress​

​xhr.upload.onprogress​​在上传阶段(即​​xhr.send()​​之后,​​xhr.readystate=2​​之前)触发,每50ms触发一次;​​xhr.onprogress​​在下载阶段(即​​xhr.readystate=3​​时)触发,每50ms触发一次。

​onload​

当请求成功完成时触发,此时​​xhr.readystate=4​

​onloadend​

当请求结束(包括请求成功和请求失败)时触发

​onabort​

当调用​​xhr.abort()​​后触发

​ontimeout​

​xhr.timeout​​不等于0,由请求开始即​​onloadstart​​开始算起,当到达​​xhr.timeout​​所设置时间请求还未结束即​​onloadend​​,则触发此事件。

​onerror​

在请求过程中,若发生​​Network error​​则会触发此事件(若发生​​Network error​​时,上传还没有结束,则会先触发​​xhr.upload.onerror​​,再触发​​xhr.onerror​​;若发生​​Network error​​时,上传已经结束,则只会触发​​xhr.onerror​​)。注意,只有发生了网络层级别的异常才会触发此事件,对于应用层级别的异常,如响应返回的​​xhr.statusCode​​是​​4xx​​时,并不属于​​Network error​​,所以不会触发​​onerror​​事件,而是会触发​​onload​​事件。

事件触发顺序

当请求一切正常时,相关的事件触发顺序如下:

  1. 触发xhr.onreadystatechange(之后每次readyState变化时,都会触发一次)
  2. 触发xhr.onloadstart
    //上传阶段开始:
  3. 触发xhr.upload.onloadstart
  4. 触发xhr.upload.onprogress
  5. 触发xhr.upload.onload
  6. 触发xhr.upload.onloadend
    //上传结束,下载阶段开始:
  7. 触发xhr.onprogress
  8. 触发xhr.onload
  9. 触发xhr.onloadend

发生​​abort​​/​​timeout​​/​​error​​异常的处理

在请求的过程中,有可能发生 ​​abort​​/​​timeout​​/​​error​​这3种异常。那么一旦发生这些异常,​​xhr​​后续会进行哪些处理呢?后续处理如下:

  1. 一旦发生aborttimeouterror异常,先立即中止当前请求
  2. 将 readystate 置为4,并触发 xhr.onreadystatechange事件
  3. 如果上传阶段还没有结束,则依次触发以下事件:
  • xhr.upload.onprogress
  • xhr.upload.[onabort或ontimeout或onerror]
  • xhr.upload.onloadend
  1. 触发 xhr.onprogress事件
  2. 触发 xhr.[onabort或ontimeout或onerror]事件
  3. 触发xhr.onloadend 事件

在哪个​​xhr​​事件中注册成功回调?

从上面介绍的事件中,可以知道若​​xhr​​请求成功,就会触发​​xhr.onreadystatechange​​和​​xhr.onload​​两个事件。 那么我们到底要将成功回调注册在哪个事件中呢?我倾向于 ​​xhr.onload​​事件,因为​​xhr.onreadystatechange​​是每次​​xhr.readyState​​变化时都会触发,而不是​​xhr.readyState=4​​时才触发。

xhr.onload = function () {
//如果请求成功
if(xhr.status == 200){
//do successCallback
}
}


上面的示例代码是很常见的写法:先判断​​http​​状态码是否是​​200​​,如果是,则认为请求是成功的,接着执行成功回调。这样的判断是有坑儿的,比如当返回的​​http​​状态码不是​​200​​,而是​​201​​时,请求虽然也是成功的,但并没有执行成功回调逻辑。所以更靠谱的判断方法应该是:当​​http​​状态码为​​2xx​​或​​304​​时才认为成功。

  xhr.onload = function () {
//如果请求成功
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
//do successCallback
}
}


结语

终于写完了......

看完那一篇长长的W3C的xhr 标准,我眼睛都花了......

希望这篇总结能帮助刚开始接触​​XMLHttpRequest​​的你。

最后给点扩展学习资料,如果你: