axios是一款优秀的HTTP请求库,它基于Promise。我们知道Promise一旦开始便不能结束,状态只能从pending -> fulfilled或pending -> rejected,那它是如何做到撤销请求呢??下面为使用axios撤销请求的两种方式。
- 可以通过CancelToken.source工厂方法创建cancel token;
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch((err) => {
if(axios.isCancel(err)) {
console.log('Request canceled', err.message);
} else {
// 处理错误
}
});
// 取消请求(message参数是可选的)
source.cancel('Operation canceled by the user.');
- 可以通过传递一个executor函数到CancelToken的构造函数来常见cancel token;
var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/1234', {
cancelToken: new CancelToken((c) => {
// executor函数接受一个cancel函数作为参数
cancel = c;
});
});
上述两种方式的本质是一样的,无非是CancelToken提供的source方法将第二种方式给封装了。 下图为axios源码的结构图,其中用红线标出来的是axios做到撤销请求的核心代码。让我们一步一步地来揭开神秘的背后。
- Cancel.js Cancel.js的代码非常简单,定义了一个构造函数,在原型上添加了一个方法toString,用于打印一些消息,以及在原型上添加了一个属性__CANCEL__,并将它的值设置为true。
'use strict';
/**
* A `Cancel` is an object that is thrown when an operation is canceled.
*
* @class
* @param {string=} message The message.
*/
function Cancel(message) {
this.message = message;
}
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};
Cancel.prototype.__CANCEL__ = true;
module.exports = Cancel;
- isCancel.js 用于判断该请求是否是已撤销的请求。
'use strict';
module.exports = function isCancel(value) {
return !!(value && value.__CANCEL__);
};
- CancelToken.js CancelToken是一个构造函数,它接受一个参数executor,且该参数得是function类型,否则抛出异常。接下来,给cancelToken构造出来的对象添加promise属性(注意:这是撤销请求的关键桥梁)。最后,执行executor函数,cancel函数首先判断请求是否是早就已撤销,然后给token添加一个reason属性,且它的值为Cancel的实例,最后,调用resolvePromise,将this.promise的状态从pending -> fulfilled。 throwIfRequested为在CancelToken原型上添加的方法,判断当前的对象上是否有reason属性,有则抛出异常。 source为在CancelToken函数上添加的方法(函数也是一个对象),它返回一个对象,具有token和cancel属性。其中,token为new CancelToken的示例,cancel为executor函数的参数c。这个参数c有什么魔力呢?
'use strict';
var Cancel = require('./Cancel');
/**
* 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;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
/**
* Throws a `Cancel` if cancellation has been requested.
*/
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
/**
* 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;
代码大部分已经贴出来,下面将结合一个例子来将整个流程给梳理出来。作者使用create-react-app构建一个简单的react项目,有两个button,分别为发送请求和撤销请求。点击发送请求后,后端的需要处理5s才能给出响应;点击撤销请求,请求被撤销。
首先,我们点击发送请求按钮,执行如下代码。
const sendRequest = () => {
axios.get('/api', {
cancelToken: new CancelToken(function(c) {
cancel = c;
})
}).then((res) => {
console.log('res: ', res);
})
};
源码首先使用默认创建的实例,调用get方法。get方法其实调用的是request方法,在request方法体中,通过调用dispatchRequest方法来发送请求。
// axios.js
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
utils.extend(instance, context);
return instance;
}
// Create the default instance to be exported
var axios = createInstance(defaults);
module.exports = axios;
// Axios.js
utils.forEach(['delete', 'get', 'head', 'options'],
function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
// this.request调用Axios.prototype.request方法
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data
}));
};
});
var dispatchRequest = require('./dispatchRequest');
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(config) {
// 省略
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
看下dispatchRequest.js的源码,看看它做了些什么事情。变量adapter为defaults.js下的默认adapter,defaults.js使用getDefaultAdapter来获取默认的adapter,getDefaultAdapter根据不同的环境拿到不同的对象来发送请求。(在本例中为XMLHttpRequest)
// dispatchRequest.js
'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
/**
* Dispatch a request to the server using the configured adapter.
*
* @param {object} config The config that is to be used for the request
* @returns {Promise} The Promise to be fulfilled
*/
module.exports = function dispatchRequest(config) {
// 省略
var adapter = config.adapter || defaults.adapter;
// 拿到当前环境下的请求对象
return adapter(config).then(function onAdapterResolution(response) {
// 省略
}, function onAdapterRejection(reason) {
// 省略
});
};
// defaults.js
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined'
&& Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
接下来看下./adapters/xhr.js文件中的源码。它里面的大部分源码挺中规中矩的,首先new XMLHttpRequest对象,然后在事件上绑定回调函数。但是有一段关于cancelToken的代码,我们在这看到了request.abort(),这个方法用于撤销请求。
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
撤销请求的源码已经全部列出来了。刚刚说到了点击发送请求的按钮,服务端得处理5s才能给出响应,在5s内,点击撤销请求的按钮。
const abortRequest = () => {
cancel();
};
cancel方法本质上调用的是CancelToken.js下的executor方法的参数cancel。它调用resolvePromise,这样一来,this.promise的状态从pending -> fulfilled。
// CancelToken.js
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
既然,this.promise的状态发生了改变,相应某块的地方的then也该执行。在xhr.js中,执行request.abort方法,触发request.onabort的回调,调用reject(createError('Request aborted', config, 'ECONNABORTED', request))。
// xhr.js
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
if (!request) {
return;
}
console.log('aborted...');
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// Clean up request
request = null;
};
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
// 由于在onabort回调中调用了reject方法,所以不会再改变状态,下面的reject可忽略
reject(cancel);
// Clean up request
request = null;
});
}
reject方法调用后,某个promise示例发生了状态变化,相应的回调函数也该执行。执行如下代码then函数的第二个参数。isCancel判断reason是否为Cancel的实例,在上面分析中可以发现reason为createError创建出来的error,所以!isCancel为true,调用throwIfCancellationRequested方法。它调用cancelToken的throwIfRequested方法,抛出异常。
// dispatchRequest.js
'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
/**
* Throws a `Cancel` if cancellation has been requested.
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
/**
* Dispatch a request to the server using the configured adapter.
*
* @param {object} config The config that is to be used for the request
* @returns {Promise} The Promise to be fulfilled
*/
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// 省略一些代码...
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// 省略
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// Transform response data
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
};
- 流程图