壹 ❀ 引
axios,一個基於promise且對ajax進行了二次封裝的http庫,在提供了與promise類似的API便捷寫法同時,它還有一大特點,便是支持取消http請求。當然取消請求並不是axios獨有特性,它也只是對於XMLHttpRequest.abort()進行了內部封裝。
我在如何做好一個基礎的搜索功能?記一個因客戶大數據量而導致的后發先至Bug一文中,對於文章問題給出的解決方案就有涉及到取消請求。雖然原生做法也能達到目的,但實際開發中我們不會用那么麻煩的寫法,自然會借助三方http庫,比如axios,而在這個庫中,我們若需要取消請求,就得使用它的cancelToken,那么這個cancelToken到底是怎么做的呢?為什么一個簡單的source.cancel調用請求就不會發起了?出於好奇,我簡單閱讀了axios對於cancelToken實現的相關源碼,那么在這篇文章做個簡單記錄。
貳 ❀ 一個取消請求的例子
了解事情原貌最簡單的做法就是寫一個請求取消的例子,然后斷點到axios中去閱讀源碼,因此我在本地項目准備了一個這樣的例子,而本地項目請求接口容易跨域,為了解決這個問題,才有了React axios 使用 http-proxy-middleware 解決跨域問題小記這篇文章。所以這里我們就不多贅述,直接結合axios實現請求取消:
// 引用CancelToken
const CancelToken = axios.CancelToken;
// 調用CancelToken.source得到一個source實例,此實例包含token和cancel兩個屬性
const source = CancelToken.source();
// 請求接口時附帶cancelToken:source.token,get與post有所區別,具體查看官方文檔
axios.get('api/request', { cancelToken: source.token })
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
alert(`Request canceled.${thrown.message}`);
}
});
// 通過source.cancel取消請求
source.cancel('Operation canceled by the user.');
上述代碼中我們實現了一個簡單接口取消請求,運行項目並打開控制台,發現並沒發起request請求,且alert正常彈出。
而實現取消接口請求也比較簡單,通過CancelToken.source()創建一個resoure實例:
// 單純創建一個實例,不用於請求,讓我們查看它
const source = CancelToken.source();
console.dir(source)
如圖,此實例包含一個名為cancel的方法,接受一個message字段,也就你要取消請求時需要傳遞的理由,當然也可以不傳。另外是一個token對象,它包含一個狀態為pending的Promise對象(這個東西作用超級大,下文會解釋這個Promise從哪來有什么用)。
取消請求的目的雖然達到了,可這幾個方法像個黑盒,它里面到底發生了什么?沒關系,讓我們順着上面請求取消的例子來一探究竟!
叄 ❀ cancelToken源碼淺析
取消接口第一步創建resouce實例,它的源碼在node_modules/axios/lib/cancel/CancelToken.js中可查看,代碼如下:
/**
* Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`.
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
當我們執行CancelToken.source()觸發的就是上述代碼,首先,我們可以看到此方法確實返回一個包含cancel與token兩個屬性的對象,讓我們來看看執行過程。
首先,此方法創建了一個calcel變量,緊接着,又創建了一個token,而token的賦值結果是調用構造函數CancelToken得到,注意,此時我們在調用構造函數時,傳遞了一個function如下:
function executor(c) {
cancel = c;
}
你可以先不用思考它有什么用,就把這個函數理解成調用構造函數傳遞了一個實參,緊接着返回了一個包含上述兩個屬性的對象,對於source方法,它做的事情就這么簡單,讓我們緊接着看看構造函數CancelToken,代碼如下:
/**
* A `CancelToken` is an object that can be used to request cancellation of an operation.
*
* @class
* @param {Function} executor The executor function.
*/
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
// 其實就外層保存resolve方法
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
// 在這里調用resolve方法,用於改變Promise狀態
resolvePromise(token.reason);
});
}
OK,對於構造函數CancelToken,它接受一個參數executor,而這個參數其實就是上面提到的executor方法。接着,函數內聲明了一個名為resolvePromise的變量。然后就是構造器屬性this.promise,既然是構造器屬性,那么這里可以預先知道,source方法中得到的token上一定有這個promise屬性(上面截圖已經展示了token中有個Promise),而這個屬性又由一個Promise構造器創建。
new Promise方法做的事情很簡單,在內部將resolve賦值給外部變量resolvePromise,我們都知道Promise接受一個callback,而callback內部可以使用resolve與reject兩個方法,而上述所做的事僅僅是將Promise內部的resolve方法暴露出去,以達到在外層改變Promise狀態的目的,比如:
// 外層定義一個變量用於保存promise內部的方法
let resolvePromise;
const p = new Promise((resolve, reject) => {
resolvePromise = resolve;
})
p.then((res) => {
console.log(res);// 我在外層改變promise狀態
});
setTimeout(() => {
// 外層調用promise的resolve方法以達到改變promise狀態目的
resolvePromise('我在外層改變promise狀態')
}, 3000)
所以簡單來說,通過這種做法我們將promise內部方法暴露出來,想在哪用就在哪用,大概如此。
讓我們繼續回到CancelToken方法,接着我們將this賦予給變量token,不要詫異,我們知道當new一個構造函數時,其實就是在隱式的給this賦值,然后返回這個this,而為了讓這種綁定與返回更為可視化,常常有如下的寫法:
function Fn(name){
const that = this;
that.name = name;
return that;
}
所以上面算是一種可視化做法,畢竟new CancelToken本身就是返回一個token並賦值給token,也相當於更好理解。
接着,我們執行了executor方法,它的參數又是一個函數,如下:
function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
// 在這里調用resolve方法,用於改變Promise狀態
resolvePromise(token.reason);
}
同學們,還記得這個executor從哪來嗎?它不就是new CancelToken時傳遞的函數嗎:
function executor(c) {
cancel = c;
}
所以這個c其實就是上面傳遞給executor的cancel方法,而cancel = c這一句,目的跟上面resolve賦值給外層一樣,也是將定義在CancelToken內部的cancel暴露出去。
你可能有點混亂了,其實這里一共做了三次方法暴露,第一次我們將resolve暴露出去,目的是讓cancel中可以通過resolvePromise來改變Promise狀態;而這個cancel也不是在定義的地方使用,這里又做了第二次暴露,將cancel拋出去賦予給了source方法,緊接着source做了第三次方法暴露,當我們開發者調用source將得到cancel與token兩個屬性。大致流程如下圖:
那么到這里我們解釋了source方法是如何產生的這兩個屬性,以及這兩個屬性分別有什么用。
所以當我們調用接口時,傳遞了一個source.token其實就是給這次請求綁定一個狀態是pending的Promise,它更像是一個開發,一旦我們調用source.cancel就會啟動這個開發,告知axios要取消這個請求,怎么取消的呢?
讓我們找到node_modules/axios/lib/adapters/xhr.js這個文件,文章開頭就說了axios是對於ajax的封裝,而ajax又是對於XMLHttpRequest那一套的封裝,所以回歸本質,在xhr.js中我們可以看到發起請求所有的准備代碼都在這里,這里大致貼一點代碼並補了注釋,我刪掉了部分對於本身意義不大的代碼:
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 准備請求附帶數據
var requestData = config.data;
// 准備請求頭
var requestHeaders = config.headers;
// 創建xhr對象
var request = new XMLHttpRequest();
// 授權相關
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
// 獲取請求地址
var fullPath = buildFullPath(config.baseURL, config.url);
// 調用open,發起請求
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 設置超時時間,你期望多久沒反應就提示超時那就設置多少
request.timeout = config.timeout;
// OK,監聽state狀態,
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// 准備response數據體
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(resolve, reject, response);
// 一旦請求結束,清空請求
request = null;
};
// 監聽abort的處理
request.onabort = function handleAbort() {
};
// 監聽error的處理
request.onerror = function handleError() {
};
// 監聽超時后的處理
request.ontimeout = function handleTimeout() {
};
// 這里就是看我們有沒有給請求附帶cancelToken,如果帶了就會走這里
if (config.cancelToken) {
// 還記得這里的promise知道是哪創建的嗎?
config.cancelToken.promise.then(function onCanceled(cancel) {
// 兩種情況,要么此時沒請求,要么請求結束被清空為null,這里就不做處理,直接返回
if (!request) {
return;
}
// 這里調用了XMLHttpRequest.abort()
request.abort();
// 這里修改的是請求體自身promise的狀態,而不是我們上文提到的那個promise
reject(cancel);
// 清空request,請求對象都不要了
request = null;
});
}
});
大家可以大致看看上述代碼,這里我們抽離出最重要的一段:
// 這里就是看我們有沒有給請求附帶cancelToken,如果帶了就會走這里
if (config.cancelToken) {
// 還記得這里的promise知道是哪創建的嗎?
config.cancelToken.promise.then(function onCanceled(cancel) {
// 兩種情況,要么此時沒請求,要么請求結束被清空為null,這里就不做處理,直接返回
if (!request) {
return;
}
// 這里調用了XMLHttpRequest.abort()
request.abort();
// 這里修改的是請求體自身promise的狀態,而不是我們上文提到的那個promise
reject(cancel);
// 清空request,請求對象都不要了
request = null;
});
}
config不用多說,其實就是我們使用axios發起請求時的配置,上文的例子也展示了,你要想取消請求,你就得給請求配置中加一個{ cancelToken: source.token },而這個token中其實就是一個狀態為pending的Promise,目的是什么我們也很清楚了。這個Promise就為了是跟當前請求進行綁定,它就像一顆遙控炸彈,而炸彈的開關就是 source.cancel,當我們調用cancel時,就會執行下面這個方法:
function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
// 啟動炸彈
resolvePromise(token.reason);
}
方法里就干一件事,將跟請求綁定在一起的Promise的狀態給改成resolve,一旦修改完成,那是不是就得跑下面這段代碼:
config.cancelToken.promise.then(function onCanceled(cancel) {
// 兩種情況,要么此時沒請求,要么請求結束被清空為null,這里就不做處理,直接返回
if (!request) {
return;
}
// 這里調用了XMLHttpRequest.abort()
request.abort();
// 這里修改的是請求體自身promise的狀態,而不是我們上文提到的那個promise
reject(cancel);
// 清空request,請求對象都不要了
request = null;
});
畢竟你Promise狀態變了,那我then就得執行,執行了干什么?request.abort()也就是XMLHttpRequest.abort()取消請求,成功把這個請求給引爆炸掉了!!!
而abort干了什么呢?很遺憾,這個就徹徹底底是個黑盒了,是瀏覽器在幫我們處理,斷點跟不進去,但通過MDN我們可以的值,當調用此方法時,如果請求已發出,那么該方法將中止請求。當一個請求被中止時,它 readyState被更改為 XMLHttpRequest.UNSENT(0) 並且請求的 status代碼被設置為 0。
肆 ❀ 總
那么到這里,我們完整跟着請求代碼把axios執行過程給講了一遍,你會發現axios在內部其實就是創建一個名為token 的 Promise與一個改變Promise狀態的方法cancel,當你需要取消一個請求時,那就把這個Promise與請求綁在一起,然后便可通過cancel改變請求上的Promise狀態,從而達到取消請求的目的,不得不說這個三方庫的作者那是真的牛媽媽給牛寶寶開門,牛到家了!!
另外,abort原生做法我測試發現若請求發出,是會取消請求,所以可以看到chrome控制台有紅色的被取消的請求,而axios直接沒請求,猜測應該是axios內部做了特殊處理,或者是我請求被取消的太快,導致根本沒發出,至於這點我就沒再去研究了。
var xhr = new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/";
xhr.open(method, url, true);
xhr.send();
if (true) {
xhr.abort();
}
結合前面文章,我們就可以在簡歷中寫通過axios.cancelToken解決XXX問題,若面試問你這個取消是怎么實現的。那真是不好意思,你算是撞到咱們的槍口上了,又可以愉快的跟面試官battle一番。OK,那么到這里,本文正式結束!!2點了!!睡覺!!
