取消请求
1. 为什么要取消请求?
请求的响应时间存在不确定性,请求次数过多时,有可能较早发起的请求会较晚响应。那么我们需要设计一套机制,确保较晚发起的请求可以在客户端就取消掉较早发起的请求。
2. 先自己想想,如何取消一次请求:
- 发起一次请求A1,且该请求中携带标识此次请求的id
- 发起一次请求A2(An表示针对同一个接口A的第n次请求),A2请求时取消原请求A1,请求A1被标识为
canceled(network process工作结束) - axios标记A1为请求异常,进入响应处理阶段(响应获取一个错误对象),请求A1结束。
3. 实际上axios是怎么做的?
针对单个请求A发多次的情形(A1,A2...An), An都会带上cancelToken(标记),A(n+1)发起时会将An请求直接canceled(不再处理An请求响应)。
4. 具体实现
axios官网的用例:
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.');
看看axios源码
// axios/lib/cancel/CancelToken.js
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
cancelToken.source()是一个工厂函数,返回了一个对象,token和cancel属性都由函数CancelToken构造而成。我们看看CancelToken构造函数做了什么:
// axios/lib/cancel/CancelToken.js
function CancelToken(executor) { // 参数executor
// 参数类型判断为函数
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
// 实例挂载一个promise,这个promise会在变量resolvePromise执行后resolved
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this; // 实例即token
// 执行器执行,将函数cancel传递到外界
executor(function cancel(message) {
if (token.reason) {
// 如果token挂载了reason属性,说明该token下的请求已被取消
return;
}
// token挂载reason属性
token.reason = new Cancel(message);
// 外界可以通过执行resolvePromise来将该token的promise置为resolved
resolvePromise(token.reason);
});
}
那么我们将这个token的promise resolved有什么用呢?
// axios/lib/adapters/xhr.js
if (config.cancelToken) {
// 请求时带上了cancelToken,如果上面token的promise resolve就会执行取消请求的操作
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
5. 最佳实践
设计一个pendings模块,在每次请求的拦截中进行请求的自动记录取消
// pendings.js
import Axios from 'axios';
const pendings = {};
export default {
/**
* 添加请求
*/
addPending(config) {
const { method, url, params, data } = config;
const id = [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
const cancel = pendings[id];
config.cancelToken = config.cancelToken || new Axios.CancelToken(function excutor(cancel) {
if (!pendings[id]) {
pendings[id] = cancel
}
})
return config;
},
/**
* 移除请求
*/
removePending(config) {
const { method, url, params, data } = config;
const id = [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
const cancel = pendings[id];
if (cancel && typeof cancel === 'function') {
cancel();
pendings.delete(identify);
}
},
/**
* 清空所有pending请求
*/
clearPending() {
Object.keys(pendings).forEach(c => pending[c]());
},
}
在你的请求模块中通过axios过滤器来使用:
// request.js
axios.interceptors.request.use((config) => {
removingPendings(config);
addPendings(config);
// ...
})
axios.interceptors.response.use((config) => {
removingPendings(config);
addPendings(config);
// ...
})
6. 总结
cancelToken说白了就是一个promise,这个promise的状态的改变交由使用者自己决定(执行cancel函数)。届时,这个promise的then回调就会执行,取消request。