前言
做一個Vue的專案時,遇到頻繁切換標籤的問題。由於不同標籤請求的ajax的結果所需時間不同,點選不同標籤時,響應時間最慢的資料會覆蓋之前響應的資料,顯示資料跟所點選標籤不對應。當時為了處理這個問題,沒想到好方法,只好控制在點選下一個標籤前,必須等前一個標籤的結果回來之後進行。
後來做API的統一管理時,看到前人寫的axios的interceptor裡有CancelToken這樣一個東西,查了查資料,發現這個可以取消請求,踏破鐵鞋無覓處,剛好可以用來處理之前遇到的頻繁切換標籤的問題。今作一記錄,也好更好的理解這個功能。
述求
點選標籤時,取消之前正在執行的請求,使得切換標籤時,頁面得到的是最後請求的結果,而不是響應最慢的結果。
用法
官方案例
- 使用 CancelToken.source 工廠方法建立 cancel token,像這樣:
// CancelToken是一個建構函式,用於建立一個cancelToken例項物件 // cancelToken例項物件包含了一個promise屬性,值為可以觸發取消請求的一個promise const CancelToken = axios.CancelToken; // 執行source()得到的是一個包含了cancelToken物件和一個取消函式cancel()的物件 // 即 source = {token: cancelToken物件, cancel: 取消函式} const source = CancelToken.source(); // 在請求的配置中配置cancelToken,那麼這個請求就有了可以取消請求的功能 axios.get('/user/12345', { cancelToken: source.token }).catch(function(thrown) { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // 處理錯誤 } }); axios.post('/user/12345', { name: 'new name' }, { cancelToken: source.token }) // 執行取消請求(message 引數是可選的) source.cancel('Operation canceled by the user.'); 複製程式碼
- 通過傳遞一個 executor 函式到 CancelToken 的建構函式來建立 cancel token
const CancelToken = axios.CancelToken; let cancel; axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // executor 函式接收一個 cancel 函式作為引數 // 把cancel函式傳遞給外面,使得外面能控制執行取消請求 cancel = c; }) }); // cancel the request cancel(); 複製程式碼
看起來有些暈,畢竟不知道里面是怎麼運作的,稍後通過原始碼解析。這裡簡單解釋就是,在請求中配置cancelToken
這個屬性,是為了使得請求具有可以取消的功能;cancelToken
屬性的值是一個CancelToken
例項物件,在它的executor
函式中提取出cancel
函式,執行這個cancel
函式來取消請求。
我的例項
點選標籤,執行getCourse
函式。點選某個標籤時,先取消之前的請求(如果之前的請求已完成,取消請求不會有任何操作)。效果是,頁面顯示的總是最後點選的標籤對應的結果。
分兩步,第一步,在get請求中配置cancelToken
屬性,開啟取消請求的功能,且在其屬性值中將cancel
函式賦給cancelRequest
,使得外部可以呼叫cancel
函式來取消請求;第二步,在執行請求前,先取消前一次的請求。
import axios from 'axios'
export default {
data() {
return {
cancelRequest: null // 初始時沒有請求需要取消,設為null
}
},
methods: {
// 點選標籤後傳送請求的函式
getCourse() {
const that = this
// 2. 準備執行新的請求前,先將前一個請求取消
// 如果前一個請求執行完了,執行取消請求不會有其他操作
if (typeof that.cancelRequest === 'function') {
that.cancelRequest()
}
// 這裡配置請求的引數,略
let params = {}
// 傳送請求
axios.get('/api/app/course',{
params: params,
cancelToken: new CancelToken(function executor(c) {
// 1. cancel函式賦值給cancelRequest屬性
// 從而可以通過cancelRequest執行取消請求的操作
that.cancelRequest = c
})
})
}
}
}
複製程式碼
一般API都會統一封裝,所以,可以將請求封裝起來
API
// /api/modules/course.js
// _this為vue元件例項物件
export function getCourseReq(params, _this) {
return axios.get('/api/app/course',{
params: params,
cancelToken: new CancelToken(function executor(c) {
// 1. cancel函式賦值給cancelRequest屬性
// 從而可以通過cancelRequest執行取消請求的操作
_this.cancelRequest = c
})
})
.then(res => {})
.catch(err => {})
}
複製程式碼
元件
import { getCourseReq } from '@/apis/modules/course'
methods: {
getCourse() {
// 2. 準備執行新的請求前,先將前一個請求取消
// 如果前一個請求執行完了,執行取消請求不會有其他操作
if (typeof this.cancelRequest === 'function') {
this.cancelRequest()
}
// 這裡配置請求的引數,略
let params = {}
// 傳送請求
getCourseReq(params, this)
.then(res => {})
.catch(err => {})
}
}
複製程式碼
遇到的坑
一開始按照上述方法寫好,但請求死活沒有取消。一遍遍核對了變數名,除錯輸出資訊,啥都對,就是沒法取消。折騰半天,想起前人配置的axios的interceptor
裡對每個請求配置了cancelToken
,目的是為了去掉重複的請求,但取消重複請求的程式碼段被註釋掉了。把cancelToken
註釋掉之後,終於雨過天晴。
axios.interceptors.request.use(
config => {
const request =
JSON.stringify(config.url) +
JSON.stringify(config.method) +
JSON.stringify(config.data || '')
// 這裡配置了cancelToken屬性,覆蓋了原請求中的cancelToken
config.cancelToken = new CancelToken(cancel => {
sources[request] = cancel
})
// if (requestList.includes(request)) {
// sources[request]('取消重複請求')
// } else {
requestList.push(request)
return config
},
error => {
return Promise.reject(error)
}
)
複製程式碼
由於interceptor會在請求傳送前做一些配置處理,這裡把原請求中的cancelToken
覆蓋了,那麼即使原請求中執行原cancelToken
的cancel
函式,由於cancelToken
物件不同了,取消操作也就無效了。後面看了原始碼可以更明白為什麼無效。
原始碼解析
根據前面的步驟,依次來看看各個原始碼是怎樣。
首先,我們為請求配置cancelToken
屬性,目的是使得請求具有能取消請求的功能,它的值是CancelToken
例項物件,那麼CancelToken
是什麼呢?
// axios/lib/cancel/CancelToken.js
'use strict';
var Cancel = require('./Cancel');
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
/**
* 定義一個將來能執行取消請求的promise物件,當這個promise的狀態為完成時(fullfilled),
* 就會觸發取消請求的操作(執行then函式)。而執行resolve就能將promise的狀態置為完成狀態。
* 這裡把resolve賦值給resolvePromise,就是為了在這個promise外能執行resolve而改變這個promise的狀態
* 注意這個promise物件被賦值給CancelToken例項的屬性promise,將來定義then函式就是通過這個屬性得到promise
*/
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
/**
* 將CancelToken例項賦值給token
* 執行executor函式,將cancel方法傳入executor,
* cancel方法可呼叫resolvePromise方法,即觸發取消請求的操作
*/
var token = this;
executor(function cancel(message) {
if (token.reason) {
// 取消已響應 返回
return;
}
token.reason = new Cancel(message);
// 這裡執行的就是promise的resolve方法,改變狀態
resolvePromise(token.reason);
});
}
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
// 這裡可以看清楚source函式的真面目
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
// c 就是CancelToken中給executor傳入的cancel方法
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
複製程式碼
CancelToken
CancelToken
是一個建構函式,通過new CancelToken()
得到的是一個例項物件,它只有一個屬性promise
, 它的值是一個能觸發取消請求的Promise物件。
token = new CancelToken(executor function) ===> { promise: Promise物件 }
執行CancelToken
函式做了兩件事:
- 建立一個
Promise
物件,且將這個物件賦值給promise
屬性,其resolve
引數被暴露出來以備cancel函式執行。 - 執行
executor
函式,將內部定義的cancel函式作為引數傳遞給executor
// 原始碼相當於: var token = this; var cancel = function (message) { if (token.reason) { // 取消動作已響應 返回 return; } token.reason = new Cancel(message); // 這裡執行的就是promise的resolve方法,改變狀態 resolvePromise(token.reason); } executor(cancel); 複製程式碼
所以執行
let cancel
token = new CancelToken(function executor(c) {
cancel = c;
});
複製程式碼
得到結果是:
token
值為{promise: Promise物件}
executor
函式被執行,即cancel = c
執行,因此變數cancel
被賦值了,值就是CanelToken
內部的那個cancel
函式。
題外話,發現Promise其實也是傳入executor,跟執行new Promise(executor)是一樣的
CancelToken.source
CancelToken.source
是一個函式,通過原始碼可以看到,執行const source = CancelToken.source()
,得到的是一個物件:
return {
token: token,
cancel: cancel
};
複製程式碼
包含一個token
物件,即CancelToken
例項物件,和一個cancel
函式。因此CancelToken.source()
函式的作用是生成token
物件和取得cancel
函式。token
物件是用於配置給axios
請求中的cancelToken
屬性,它包含了promise物件,cancel
函式是將來觸發取消請求的函式。
token和cancel的關係是,token裡存放了取消請求的promise,cancel裡存放了將promise的狀態改變的resolve函式,一個是執行體,一個是開關。開關觸動,執行體的狀態就改變,其相應的then函式就執行。他們就是通過promise的狀態特性進行關聯的。
如何執行取消請求的
萬事俱備,就差如何執行取消請求了。這也是我一開始最疑惑的地方,一起來看看吧。下面是開始發起請求的原始碼:
// axios/lib/adapters/xhr.js
// 建立XHR物件
var request = new XMLHttpRequest()
// 模擬當前ajax請求
request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true)
// 定義取消請求promise物件的then方法
if (config.cancelToken) { // 如果配置了cancelToken屬性
// 當promise為完成態時,這個then函式執行,即執行取消請求
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
// 取消ajax請求
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
複製程式碼
這是最終執行ajax請求的地方。
我們前面配置了cancelToken = new CancelToken(executor)
,得到這個物件
cancelToken = {promise: 取消請求的Promise物件}
複製程式碼
因此上面cancelToken.promise
就是這個promise
。
我們知道,Promise
的then
函式是在Promise
物件處於fullfilled
時執行,這就是暗中的關聯。執行cancel
函式,會將Promise
的狀態置為fullfilled
,這裡定義的then
函式就會執行,從而取消請求。
攔截器問題
之前遇到的問題,攔截器中配置的cancelToken
覆蓋了請求中的cancelToken
,導致請求失效。原因是,攔截器重新定義後,cancelToken
屬性值變成了新的token,cancelToken.promise
也就變成了新的promise
。then
函式是在攔截器之後、真正傳送請求之前定義的,因此這時定義的then
函式,是對應這個新的Promise物件的,於是原來token的promise並沒有then函式,那麼執行原cancel
函式把原Promise物件置為了fullfilled
狀態,但沒有相應的then函式執行,有人發訊號,沒人執行,取消請求無效。
axios流程:定義請求 -> 請求攔截器 -> 傳送請求 -> 響應攔截器 -> 接收響應
後語
一入原始碼深似海,看完真是受益匪淺,這裡面的一些思想、做法,多可以學習借鑑。