歡迎您光臨本站 註冊首頁

JS XMLHttpRequest原理與使用方法深入詳解

←手機掃碼閱讀     zmcjlove @ 2020-05-04 , reply:0

你真的會使用XMLHttpRequest嗎?
看到標題時,有些同學可能會想:「我已經用 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.responseType 為 json
部分瀏覽器不支援設定請求超時,即無法使用 xhr.timeout
部分瀏覽器不支援 xhr.responseType 為 blob
細說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個方法看起來簡單,但卻處處是坑兒。
你是否遇到過下面的坑兒?――反正我是遇到了。。。
使用 getAllResponseHeaders() 看到的所有 response header 與實際在控制檯 Network 中看到的 response header 不一樣 使用 getResponseHeader() 獲取某個 header 的值時,瀏覽器拋錯 Refused to get unsafe header "XXX"
經過一番尋找最終在 Stack Overflow找到了答案。
" 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個需要注意的坑兒:
可以在 send() 之後再設定此 xhr.timeout ,但計時起始點仍為呼叫 xhr.send() 方法的時刻。 當 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; };
從程式碼中我們可以看出:
XMLHttpRequestEventTarget 介面定義了7個事件: onloadstart
onprogress
onabort
ontimeout
onerror
onload
onloadend 每一個 XMLHttpRequest 裡面都有一個 upload 屬性,而 upload 是一個 XMLHttpRequestUpload 物件 XMLHttpRequest 和 XMLHttpRequestUpload 都繼承了同一個 XMLHttpRequestEventTarget 介面,所以 xhr 和 xhr.upload 都有第一條列舉的7個事件 onreadystatechange 是 XMLHttpRequest 獨有的事件
所以這麼一看就很清晰了:
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 事件。
事件觸發順序
當請求一切正常時,相關的事件觸發順序如下:
觸發 xhr.onreadystatechange (之後每次 readyState 變化時,都會觸發一次) 觸發 xhr.onloadstart
//上傳階段開始: 觸發 xhr.upload.onloadstart 觸發 xhr.upload.onprogress 觸發 xhr.upload.onload 觸發 xhr.upload.onloadend
//上傳結束,下載階段開始: 觸發 xhr.onprogress 觸發 xhr.onload 觸發 xhr.onloadend
發生abort/timeout/error異常的處理
在請求的過程中,有可能發生 abort / timeout / error 這3種異常。那麼一旦發生這些異常, xhr 後續會進行哪些處理呢?後續處理如下:
一旦發生 abort 或 timeout 或 error 異常,先立即中止當前請求 將 readystate 置為 4 ,並觸發 xhr.onreadystatechange 事件 如果上傳階段還沒有結束,則依次觸發以下事件: xhr.upload.onprogress
xhr.upload.[onabort或ontimeout或onerror]
xhr.upload.onloadend 觸發 xhr.onprogress 事件 觸發 xhr.[onabort或ontimeout或onerror] 事件 觸發 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 } }


[zmcjlove ] JS XMLHttpRequest原理與使用方法深入詳解已經有269次圍觀

http://coctec.com/docs/javascript/show-post-232811.html