溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

深入講解xhr(XMLHttpRequest)/jsonp請(qǐng)求之a(chǎn)bort

發(fā)布時(shí)間:2020-10-05 04:07:13 來(lái)源:腳本之家 閱讀:1376 作者:Jin 欄目:web開發(fā)

前言

相信大家在工作中經(jīng)常需要使用AJAX,所以當(dāng)大家看到文章標(biāo)題的時(shí)候可能會(huì)覺得這是一個(gè)老生常談的話題。

前端開發(fā)中向后端發(fā)起xhr(XMLHttpRequest)請(qǐng)求(代表性的就是熟悉的ajax)是再正常不過(guò)的事。

但在前端開發(fā)過(guò)程中,不怎么重視xhr的abort(中止掉xhr請(qǐng)求,及表示取消本次請(qǐng)求)。往往會(huì)帶來(lái)一些不可意料的結(jié)果。

比如:切換tab,發(fā)起xhr請(qǐng)求,渲染同一個(gè)列表。就這么簡(jiǎn)單的拉取數(shù)據(jù)渲染列表的功能,并且可以根據(jù)tab切換。想想應(yīng)該是很簡(jiǎn)單。但是假如你只顧著發(fā)起xhr請(qǐng)求,而沒(méi)有abort掉它,想想會(huì)發(fā)生什么。很有可能就是當(dāng)前選中的tab數(shù)據(jù),并不是你想要的。說(shuō)白了就是數(shù)據(jù)錯(cuò)了。這時(shí)候你可能就要考慮是不是xhr請(qǐng)求返回?cái)?shù)據(jù)的順序問(wèn)題。

答案是肯定的,xhr請(qǐng)求返回?cái)?shù)據(jù)順序是不固定的。所以你要做的就是abort掉你之前的xhr請(qǐng)求,然后再發(fā)起一個(gè)新的xhr請(qǐng)求。

結(jié)合上面所說(shuō)的例子可以知道xhr使用不當(dāng)會(huì)存在以下問(wèn)題:

  • 容易出現(xiàn)頁(yè)面最終數(shù)據(jù)與狀態(tài)不一致的問(wèn)題,這可能再列表篩選是出現(xiàn)的概率比較大。
  • xhr請(qǐng)求達(dá)到一定數(shù)量之后,瀏覽器就會(huì)顯得非常的慢。因?yàn)橛刑嗟恼?qǐng)求在請(qǐng)求服務(wù)器資源。

為了解決上面的問(wèn)題,我們?cè)谶M(jìn)行頁(yè)面的時(shí)候就必須考慮abort掉所有的xhr請(qǐng)求。

那么如何實(shí)現(xiàn)xhr的abort方法呢,或者通過(guò)何種方式abort掉xhr呢?

一個(gè)簡(jiǎn)單的xhr

我們都知道,現(xiàn)在的框架(例如:jQuery的ajax模塊)對(duì)xhr都進(jìn)行了封裝,是為了讓我們更好的使用xhr。但是也蒙蔽了我們的眼睛。讓我們拋開框架,來(lái)看看一個(gè)簡(jiǎn)單的xhr怎么實(shí)現(xiàn)。

//僅供參考 xhr
function ajax(type ,url , data , successCallBack , errorCallBack){
 let xhr = new XMLHttpRequest();
 xhr.onload = ()=>{
 if(xhr.status === 200){
  return successCallBack(xhr.response||xhr.responseText);
 }
 return errorCallBack('請(qǐng)求失敗');
 }
 xhr.onerror = ()=>{
 return errorCallBack('出錯(cuò)了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
}

這就是一個(gè)簡(jiǎn)單的xhr請(qǐng)求的實(shí)現(xiàn),我把它命名為ajax,我們現(xiàn)在可以通過(guò)以下方式進(jìn)行調(diào)用:

ajax('get','/test/getUserList' , undefined , function(result){
 console.log('成功了。', result);
} ,function(error){
 console.log(error);
});

如果使用這個(gè)方法我們是沒(méi)辦法abort掉xhr請(qǐng)求的。好吧,現(xiàn)在我們把它改造一下,讓它支持abort方法:

//僅供參考 xhr.abort
function ajax(type ,url , data , successCallBack , errorCallBack){
 let xhr = new XMLHttpRequest();
 xhr.onload = ()=>{
 if(xhr.status === 200){
  return successCallBack(xhr.response||xhr.responseText);
 }
 return errorCallBack('請(qǐng)求失敗');
 }
 xhr.onerror = ()=>{
 return errorCallBack('出錯(cuò)了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 return xhr;//返回XMLHttpRequest實(shí)例對(duì)象
}

好像沒(méi)有什么變化對(duì)吧。不錯(cuò),只要在函數(shù)的末尾添加return xhr;將XMLHttpRequest實(shí)例對(duì)象返回即可。那我們?cè)诰鸵呀?jīng)可以如愿的abort掉xhr請(qǐng)求。

let xhr = ajax('get','/test/getUserList' , undefined , function(result){
 console.log('成功了。', result);
} ,function(error){
 console.log(error);
});
//abort
xhr.abort();

好像我們已經(jīng)大功告成了。但是問(wèn)題來(lái)了,現(xiàn)在Promise這么好用,為什么不把它加進(jìn)來(lái)呢。像這樣沒(méi)法在我們的Promise鏈?zhǔn)秸{(diào)用上使用它。

Promise封裝xhr

好了,現(xiàn)在的首要任務(wù)是封裝出一個(gè)Promise版的ajax庫(kù)。首要要確認(rèn)的就是,ajax方法需要返回的是Promise實(shí)例對(duì)象,而不再是原生的XMLHttpRequest實(shí)例對(duì)象。知道了這一點(diǎn)那就可以進(jìn)行封裝了。

//僅供參考 promise
function ajax(type ,url , data ){
 let xhr = new XMLHttpRequest();
 let promise = new Promise(function(resolve , reject){
 xhr.onload = ()=>{
  if(xhr.status === 200){
  return resolve(xhr.response||xhr.responseText);
  }
  return reject('請(qǐng)求失敗');
 }
 xhr.onerror = ()=>{
  return reject('出錯(cuò)了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 });
 return promise;//返回Promise實(shí)例對(duì)象
}

使用了Promise之后我們不再需要傳入回調(diào)函數(shù)。所以參數(shù)減少了。這樣我們就可以愉快的進(jìn)行鏈?zhǔn)秸{(diào)用了。

let promise = ajax('get','/test/getUserList');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})

可問(wèn)題又來(lái)了,Promise實(shí)例是沒(méi)有abort方法的。假如我們把a(bǔ)jax方法修改為返回xhr,我們是可以如期調(diào)用abort方法殺死請(qǐng)求,但是我們就不能使用Promise帶給我們的好處了。

仔細(xì)思考,最后一句return promise; 這里是不能改。我們只能另外想辦法。

最簡(jiǎn)單的解決方式就是創(chuàng)建一個(gè)xhr和promise的映射關(guān)系。也就是每一個(gè)promise對(duì)應(yīng)一個(gè)唯一的xhr請(qǐng)求。有了思路之后,解決方案就來(lái)了。

let map = [];//用于保存promise和xhr之間的映射關(guān)系
//僅供參考 promise abort
function ajax(type ,url , data ){
 let xhr = new XMLHttpRequest();
 let promise = new Promise(function(resolve , reject){
 xhr.onload = ()=>{
  if(xhr.status === 200){
  return resolve(xhr.response||xhr.responseText);
  }
  return reject('請(qǐng)求失敗');
 }
 xhr.onerror = ()=>{
  return reject('出錯(cuò)了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 });
 map.push({promise:promise,request:xhr});//創(chuàng)建promise和xhr之間的映射關(guān)系,保存到全局的一個(gè)數(shù)組中。
 return promise;//返回Promise實(shí)例對(duì)象
}
//abort 請(qǐng)求
function abort(promise){
 for(let i = 0 ; i < map.length ; i++ ){
 if ( map[i].promise === promise ){
  map[i].request.abort();
 }
 }
}

通過(guò)在全局創(chuàng)建一個(gè)map保存所有的promise和xhr之間的映射關(guān)系。這樣我們就可以在需要abort請(qǐng)求的時(shí)候根據(jù)映射關(guān)系找到xhr并abort請(qǐng)求。

let promise = ajax('get','/test/getUserList');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})
abort(promise);

好吧,到這里Promise版的ajax,我們已經(jīng)實(shí)現(xiàn)了。是不是很簡(jiǎn)單啊。

何為jsonp

假如你還不明白jsonp是何物,那希望下面的篇幅能讓你明白??赡苣懔阈堑闹揽缭秸?qǐng)求,但是可能沒(méi)有在實(shí)戰(zhàn)中碰到過(guò)。那么我們先來(lái)看看,一個(gè)簡(jiǎn)單的jsonp函數(shù)是怎么實(shí)現(xiàn)的吧。

let index = 0;
//僅供參考 jsonp
function jsonp(url,jsonp,successCallback , errorCallback){
 let script = document.createElement('script');
 let result ;
 script.onload = function(){
 successCallback(result);
 }
 script.onerror = function(){
 errorCallback('出錯(cuò)了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿給后端進(jìn)行輸出執(zhí)行的。
 result = Array.prototype.slice.call(arguments);
 }
 document.head.append(script);
}

jsonp算起來(lái)應(yīng)該就是通過(guò)script加載實(shí)現(xiàn)的跨域請(qǐng)求。其中重要的就是數(shù)據(jù)返回的接收,我們需要和后端開發(fā)同學(xué)協(xié)商回調(diào)函數(shù)的變量名。然后后端獲取到回調(diào)函數(shù)名,并且在返回時(shí)把回調(diào)函數(shù)和數(shù)據(jù)拼接成字符串返回到前端。前端我們添加一個(gè)window對(duì)象的函數(shù)用于接收數(shù)據(jù),在函數(shù)執(zhí)行完成后,就會(huì)觸發(fā)script.onload事件,這樣就可以真正執(zhí)行用戶回調(diào)函數(shù)了。

可能你會(huì)覺得有點(diǎn)繞,其實(shí)細(xì)細(xì)的理一下,應(yīng)該就明白了。

后端其實(shí)很簡(jiǎn)單,只要獲取到j(luò)sonp函數(shù)變量名就可以了。然后把函數(shù)和數(shù)據(jù)拼接成字符串返回即可。

下面我們來(lái)看看Node.js中的實(shí)現(xiàn):

let query = ctx.request.query;
let jsonp = query.jsonp;//與后端協(xié)商的回調(diào)參數(shù)
ctx.body = jsonp+'({code:0,msg:"success"})';

這個(gè)回調(diào)函數(shù)并不是用戶輸入的successCallback,而是jsonp函數(shù)內(nèi)部的window[callBackName] ,為什么要這樣。你細(xì)想一下JavaScript的作用域應(yīng)該就會(huì)知道。這就好比你在script標(biāo)簽中執(zhí)行一個(gè)函數(shù)一樣。

有可能我們第一次調(diào)用jsonp函數(shù)服務(wù)器會(huì)返回如下結(jié)果:

<script >
 //只有這一行是服務(wù)器返回的,
 //script標(biāo)簽是document.head.append(script)時(shí)候加的
 jsonpCallback0({code:0,msg:"success"});
</script>

所以,得出結(jié)論就是:函數(shù)必須能通過(guò)window對(duì)象上訪問(wèn)到。不然執(zhí)行時(shí)就會(huì)報(bào)錯(cuò)。這就是為什么我們不能直接把用戶傳入的回調(diào)直接用來(lái)當(dāng)成回調(diào)接收數(shù)據(jù)的真正原因。

再次強(qiáng)調(diào):JavaScript作用域。

一次成功的jsonp應(yīng)該是:添加script標(biāo)簽到head,后端接收到j(luò)sonp數(shù)據(jù),返回拼接好的函數(shù)名和數(shù)據(jù)字符串,執(zhí)行window對(duì)象上的函數(shù)拿到數(shù)據(jù),執(zhí)行script.onload事件,執(zhí)行成功回調(diào)。

jsonp的abort方法何去何從

現(xiàn)在你已經(jīng)知道了jsonp的原理了。那么如何才能對(duì)script加載數(shù)據(jù)進(jìn)行abort呢。

犯難的問(wèn)題來(lái)了,script并沒(méi)有真正的abort方法給我們使用。我們所做的就是盡最大的努力提供類似于abort功能的方法。

思路就是使用Event事件對(duì)象。觸發(fā)script的error監(jiān)聽事件。所以我們得對(duì)jsonp函數(shù)添加一個(gè)trigger輔助函數(shù)進(jìn)行觸發(fā)error事件。

//[trigger 觸發(fā)事件]
function trigger(element,event){
 if( !isString(event) ) {
 return;
 }
 if ( element.dispatchEvent ){
 let evt = document.createEvent('Events');// initEvent接受3個(gè)參數(shù)
 evt.initEvent(event, true, true);
 element.dispatchEvent(evt);
 }else if ( element.fireEvent ){ //IE
 element.fireEvent('on'+event);
 }else{
 element['on'+event]();
 }
}
let index = 0;
//僅供參考 jsonp.abort
function jsonp(url,jsonp,successCallback , errorCallback){
 let script = document.createElement('script');
 let result ;
 script.onload = function(){
 successCallback(result);
 }
 script.onerror = function(){
 errorCallback('出錯(cuò)了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿給后端進(jìn)行輸出執(zhí)行的。
 result = Array.prototype.slice.call(arguments);
 }
 script.abort = ()=>{
 return trigger(script,'error');
 };
 document.head.append(script);
 return script;
}

我們把Promise也使用進(jìn)來(lái),那樣的話,我們就可以脫離回調(diào)地獄了不是嗎?

let index = 0;
//僅供參考 jsonp.abort
function jsonp(url,query,jsonp){
 let script = document.createElement('script');
 let result ;
 let promise = new Promise(function(resolve,reject){
 script.onload = function(){
  return resolve(result);
 }
 script.onerror = function(){
  return reject('出錯(cuò)了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿給后端進(jìn)行輸出執(zhí)行的。
  result = Array.prototype.slice.call(arguments);
 }
 document.head.append(script);
 });
 script.abort = ()=>{
 return trigger(script,'error');
 };
 map.push({promise:promise,request:script});//創(chuàng)建promise和script之間的映射關(guān)系,保存到全局的一個(gè)數(shù)組中。
 return promise;
}

同樣的我們套用上面的xhr的abort函數(shù)封裝。這樣我們就大功告成了?;镜墓δ芪覀兙腿繉?shí)現(xiàn)了。我們就可以開始進(jìn)行調(diào)用了。

let promise = jsonp('/test/getUserList','jsonp');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})
abort(promise);

總結(jié)

雖然,我們已經(jīng)完成了封裝,但是還有很多的意外沒(méi)有考慮,要想再實(shí)戰(zhàn)中運(yùn)用還必須進(jìn)行封裝和重構(gòu)。我們必須重視abort方法在xhr/jsonp中的運(yùn)用,但是也不能濫用,適可而止。存在多層服務(wù)器調(diào)用時(shí),應(yīng)該更需要慎重考慮。

要想了解更多,可以參考這是我封裝好的一個(gè)Promise版本的ajax/jsonp庫(kù)https://github.com/Yi-love/xhrp,大家也可以通過(guò)本地進(jìn)行下載。

好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)億速云的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI