axios
提供 CancelToken 方法可以取消正在发送中的接口请求。
官方提供了两种方式取消发送,第一种方式如下:
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.');
第二种方式如下:
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();
官方实现取消功能的文件存放在 /lib/cancel/CancelToken.js
虽然代码不多,但是第一次看真是一头雾水,下面就来抽丝剥茧,一步步还原里面的实现逻辑。
分析
两种方式都调用了 CancekToken
这个构造函数,我们就先从这个构造函数开始。
分析:
第一种方式:
CancekToken
提供一个静态方法source
,source
方法返回token
和cancel
方法
第二种方式:
CancekToken
接收一个回调函数作为参数,回调函数接收cancel
取消方法
第二种方式更容易入手,我们可以先实现构造函数CancekToken
,再考虑第一种方式静态方法source
的实现。
简易版 axios
首先我们写个简易版的axios,方便我们后面的分析和调试:
function axios(url,config){
return new Promise((resolve,reject)=>{
const xhr = new XMLHttpRequest();
xhr.open(config.method || "GET",url);
xhr.responseType = config.responseType || "json";
xhr.onload = ()=>{
if(xhr.readyState === 4 && xhr.status === 200){
resolve(xhr.response);
}else{
reject(xhr)
}
};
xhr.send(config.data ? JSON.stringify(config.data) : null);
})
}
CancelToken
第二种方式中,我们可以看到 CancelToken
在配置参数cancelToken
中实例化:
axios.get('/user/12345', {
cancelToken: new CancelToken
});
所以在axios
中,我们也会根据配置中是否包含cancelToken
来取消发送:
function axios(url,config){
return new Promise((resolve,reject)=>{
const xhr = new XMLHttpRequest();
...
if(config.cancelToken){
// 如果存在 cancelToken 参数
// xhr.abort() 终止发送任务
// reject() 走reject方法
}
...
回到配置参数,CancelToken
接受一个回调函数作为参数,参数包含取消的cancel
方法,我们初始化CancelToken
方法如下:
function CancelToken(executor){
let cancel = ()=>{};
executor(cancel)
}
回到官方例子,例子中参数cancel
方法被赋值给当前环境的cancel
变量,于是当前环境cancel
变量指向CancelToken
方法中的cancel
函数表达式。
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
cancel = c; // 指向CancelToken中的 cancel 方法
})
});
接下来cancel
方法一旦被执行,就能触发请求终止(发布订阅)。
这里官方源码巧妙的使用了Promise
链式调用的方式实现,我们给CancelToken
方法返回一个Promise
方法:
function CancekToken(executor){
let cancel = ()=>{};
const promise = new Promise(resolve => cancel = resolve);
executor(cancel);
return promise;
}
接下来只要用户执行cancel
方法,配置参数cancelToken
获得的Promise方法就能响应了:
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
cancel = c; // 指向CancelToken中的 cancel 方法
})
});
// 执行
+ cancel("canceled request");
这里可以把 cancel 理解成 Promise.resolve
axios中响应cancel
方法:
function axios(url,config){
return new Promise((resolve,reject)=>{
const xhr = new XMLHttpRequest();
...
if(config.cancelToken){
+ config.cancelToken.then(reason=>{
+ xhr.abort();
+ reject(reason);
})
}
...
关键点是把 Promise.resolve 从函数内部抽出来,巧妙的实现了异步分离
到了这里,第二种方法的取消功能就基本实现了。
CancekToken.source
source
作为 CancekToken 提供的静态方法,返回token
和 cancel
方法。
cancel
方法跟前面的功能是一样的,可以理解成局部环境里面声明好cancel
再抛出来。
我们再来看看第二种方式 token
在配置中的使用:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token // token 返回的是CancelToken实例
})
根据前面的配置我们可以知道 source.token
实际上返回的是 CancelToken
实例。
了解 source
方法需要返回的对象功能后,就可以轻松实现source
方法了:
CancekToken.source = function(){
let cancel = ()=>{};
const token = new CancekToken(c=>cancel = c);
return {
token,
cancel
}
}
axios.isCancel
通过上的代码我们知道,取消请求会走reject
方法,在Promise中可以被catch
到,不过我们还需要判断catch
的错误是否来自取消方法,这里官方提供了isCancel
方法判断:
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (error) {
// 判断是否 取消操作
if (axios.isCancel(error)) {}
});
在js中我们可以通过instanceof
判断是否来自某个构造函数的实例,这里新建Cancel
方法来管理取消发送的信息:
function Cancel(reason){
this.message = reason;
}
CancekToken.source
返回的cancel
方法通过函数包装,实例化一个Cancel
作为取消参数:
CancekToken.source = function(){
- let cancel = ()=>{};
- const token = new CancekToken(c=>cancel = c);
+ let resolve = ()=>{};
+ let token = new CancekToken(c=>resolve = c);
return {
token,
- cancel,
+ cancel:(reason)=>{
+ // 实例化一个小 cancel,将 reason 传入
+ resolve(new Cancel(reason))
+ }
}
}
最终Promise.catch
到的参数来自实例Cancel
,就可以很容易的判断error
是否来自Cancel
了:
function isCancel(error){
return error instanceof Cancel
}
// 将 `isCancel` 绑定到 axios
axios.isCancel = isCancel
最后,官方还判断了CancelToken.prototype.throwIfRequested
,如果调用了cancel
方法,具有相同cancelToken
配置的ajax请求也不会被发送,这里可以参考官方代码的实现。
全部代码
最后是全部代码实现:
function Cancel(reason) {
this.message = reason
}
function CancekToken(executor) {
let reason = null
let resolve = null
const cancel = message => {
if(reason) return;
reason = new Cancel(message);
resolve(reason)
}
const promise = new Promise(r => (resolve = r))
executor(cancel)
return promise
}
CancekToken.source = function() {
let cancel = () => {}
let token = new CancekToken(c => (cancel = c))
return {
token,
cancel
}
}
const source = CancekToken.source()
axios('/simple/get', {
cancelToken: source.token
}).catch(error => {
if (axios.isCancel(error)) {
console.log(error)
}
})
source.cancel('canceled http request 1')
let cancel
axios('/simple/get', {
cancelToken: new CancekToken(c => {
cancel = c
})
}).catch(error => {
if (axios.isCancel(error)) {
console.log(error)
}
})
cancel('canceled http request 2')
function axios(url, config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(config.method || 'GET', url)
xhr.responseType = config.responseType || 'json'
if (config.cancelToken) {
config.cancelToken.then(reason => {
xhr.abort()
reject(reason)
})
}
xhr.onload = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr)
}
}
xhr.send(config.data ? JSON.stringify(config.data) : null)
})
}
axios.isCancel = function(error) {
return error instanceof Cancel
}
es6简易版本的实现:
export class Cancel {
public reason:string;
constructor(reason:string){
this.reason = reason
}
}
export function isCancel(error:any){
return error instanceof Cancel;
}
export class CancelToken {
public resolve:any;
source(){
return {
token:new Promise(resolve=>{
this.resolve = resolve;
}),
cancel:(reason:string)=>{
this.resolve(new Cancel(reason).reason)
}
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。