Axios 文檔案例
先看下 Axios 文檔給的例子 https://github.com/axios/axios#cancellation
- 通過
CancelToken.source
工廠函數進行取消
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
- 通過
CancelToken
構造函數進行取消
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
// cancel the request
cancel();
- 通過
AbortController
中斷請求,這是fetch
的 api,本文就不再詳細介紹了,具體使用可以參考 https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController/AbortController
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
源碼分析
首先需要從 GitHub 下載 Axios 源碼。如果不想下載,也可以打開 https://github1s.com/axios/axios/ 進行查看。
工廠函數 CancelToken.source
通過前面兩個例子,可以知道取消請求和 CancelToken
這個類息息相關,CancelToken.source()
工廠函數只不過是在我們看不見的內部幫助我們去實例化一個 CancelToken
的實例出來。
那我們先來看下工廠函數的實現。
// 文件路徑 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
};
};
module.exports = CancelToken;
可以看到 CancelToken.source
這個工廠函數就是幫助我們實例化了一個 CancelToken
的實例,然後返回給我們需要使用的 實例(token) 和 取消請求的函數(cancel) 。
接下來我們繼續深入 CancelToken
內部,看看爲什麼執行了 cancel
函數後,請求就中斷了。
CancelToken 類
// 文件路徑 Axios/lib/cancel/CancelToken.js
// ...
/**
* 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.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
// eslint-disable-next-line func-names
this.promise.then(function(cancel) {
if (!token._listeners) return;
var i;
var l = token._listeners.length;
for (i = 0; i < l; i++) {
token._listeners[i](cancel);
}
token._listeners = null;
});
// eslint-disable-next-line func-names
this.promise.then = function(onfulfilled) { // ...
};
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
// ...
通過文檔中的例子可以看出,在發起請求的時候傳入了 cancelToken
,也就是 CancelToken
的一個實例。
實例化的過程中會調用我們傳入的 executor
函數,將 cancel
函數傳遞給我們外部。
另外這個實例上有一個 promise
的屬性,當我們調用 cancel
函數,promise
則會從 pending
的狀態變成 fulfilled
。從而觸發 promise.then
,執行所有的 token._listeners
。
token._listeners
又從何而來?
答案還是在當前的文件中
// 文件路徑 Axios/lib/cancel/CancelToken.js
// ...
/**
* Subscribe to the cancel signal
*/
CancelToken.prototype.subscribe = function subscribe(listener) {
// reason 值不爲 undefined 說明該請求已取消,可直接調用 listener
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
};
// ...
在 CancelToken
的原型對象上添加了 subscribe
方法,用於訂閱取消請求的事件。如果該請求已被取消,則會立即調用 listener
,否則會將 listener
保存在 _listeners
數組中。
當我們調用 cancel
也就是取消請求的時候,_listeners
中保存的 listener
則會被調用(見上文)。
這時候並沒有看到中斷請求的操作,具體的邏輯是在 listener
內部,這樣寫的原因就是可以進行解耦,提高代碼的複用性。
另外還有一個 unsubscribe
取消訂閱就不再展開了。
這就是典型的訂閱發佈模式。
取消請求
最快速的方法就是搜索 config.cancelToken.subscribe
,這樣就可以快速定位到取消請求的具體實現。
只搜索 lib 文件夾即可,可以看到有兩處地方,一個是 lib/adapters/http.js
,另一個是 lib/adapters/xhr.js
。
因爲 Axios 是一個支持 node.js 和瀏覽器的 http 客戶端。這裏應用了適配器的模式來兼容這兩個平臺。本文研究的是取消請求,就不去深究這部分了,我們看其中之一就好了。
// Axios/lib/adapters/xhr.js
// ...
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
// ...
從 187 行這裏開始,我們可以看到 config.cancelToken.subscribe(onCanceled)
往 cancelToken
註冊了一箇中斷請求的回調。request.abort();
這裏的 request
是 XMLHttpRequest
的一個實例。
另外還有一個函數 done
,即請求成功或者失敗之後會將上面註冊的 onCanceled
進行取消註冊。
至此整個取消請求的邏輯就跑通了。我簡單畫了個圖(畫了幾個小時),希望能方便大家理解。
結合 Vue 實現離開頁面取消未完成的請求
思路就是利用一個對象來管理所有的 CancelToken
實例。發起請求之前,把新創建的 cancelToken
保存到對象中,請求結束後(包括成功、失敗)把對應的實例清除。
再結合 vue-router 的路由守衛,這樣就可以在離開頁面的時候取消所有未完成的請求。
有些全局的接口需要做特殊處理,比如請求用戶信息之類的,這些全局通用的接口就不能再離開頁面的時候中斷請求。
具體代碼這裏就不展示了。我寫了一個 demo,有需要的小夥伴可以自行查看。
https://github.com/AD-feiben/demos/tree/main/abort-req
最後再重申一點,學習源碼是爲了學習源碼中優秀的設計,我們需要思考如何將這個設計應用到我們的項目中,這纔是最重要的一點。
希望文章的內容能爲你提供一絲絲幫助,如果錯誤,還望指正。