# Axios 源码分析
何建贤 / 2020-11-15
Axios (opens new window) 可以说是当前 Github 上最受关注的 HTTP 库,目前已经有超过 78k 的 star 数。作为 vue.js 官方推荐的 HTTP 库,必然有着过人之处。
Axios 的主要特性包括:
- 基于 Promise
- 支持浏览器和 node.js
- 可添加拦截器和转换请求和响应数据
- 请求可以取消
- 自动转换 JSON 数据
- 客户端支持防范 XSRF
- 支持各主流浏览器及 IE8+
对比于 fetch,除了同样支持 Promise API 外,aixos 的确拥有更加丰富的功能,而这次的源码分析也主要是针对‘拦截器’和‘请求取消’。
# 拦截器
在 Axios 中,大概是这样添加拦截器:
// Add a request interceptor
axios.interceptors.request.use(function (config) {
return config;
}, function (error) {
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
return Promise.reject(error);
});
从上面的代码,我们可以把每一次请求想象成一条管道里的流过的水。当一个请求发出的时候,会先流过 interceptors 的 request 部分,接着请求会发出,当接受到响应时,会先流过 interceptors 的 response 部分,最后返回,这条管道大概如下:
interceptors.request -> request -> interceptors.response -> response
而 Axios 内部,很巧妙地实现了上面所说的管道式流程,首先看 lib/core/Axios.js
文件里的部分代码:
var dispatchRequest = require('./dispatchRequest');
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {
// 省略部分代码...
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
// 省略部分代码...
上面的的代码中,首先看到 Axios 类的定义里面有 interceptors,并且带有 request 和 response 属性,而这 2 个属性都是指向 InterceptorManager 类的实例。这个等下再说,先看 request 方法里 interceptors 的实现。
我们留意 chain 这个变量,经过 2 次 forEach
操作后,最后的值会变成:(这个 forEach 方法在 InterceptorManager 中定义)
[interceptor.request.fulfilled, interceptor.request.rejected,
dispatchRequest, undefined,
interceptor.response.fulfilled, interceptor.response.rejected]
而最终将 chain 里的方法包成 promise,并返回:
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
返回的 promise 会是这样:
Promise.resolve(config)
.then(interceptor.request.fulfilled, interceptor.request.rejected)
.then(dispatchRequest, undefined)
.then(interceptor.response.fulfilled, interceptor.response.rejected)
讲到这里,就差一步了,就是发起请求,截取部分代码:
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
// 省略 'post', 'put', 'patch' 方法的定义
到这里,我们可以看到,axios 将所有的请求都通过 request 方法发起, 这样就完全符合我们在上面说的管道流程,而 chain
变量里定义的 undefined
也是很巧妙。而在lib/core/InterceptorManager.js
里,定义了 InterceptorManager 类,和一些方法,例如 use 和 eject,包括上面的 forEach。
# 取消请求
首先还是从调用方法开始看:
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
// something
});
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
分析上面的这段代码,可以看得出CancelToken.source()
是起最主要作用的,包括它的token
和cancel
方法。我一开始以为取消请求,会是在拦截器的那条管道上去处理,但是实际上并没有,它是把这取消请求的逻辑放到了 adapter(这里主要分析 xhr.js,也就是浏览器部分) 来处理,而 adapter 会在 dispatchRequest 里引用。
其实这一部分代码并不多,但是理解起来会有点乱,首先看 lib/adapters/xhr.js
这个文件,截取部分代码:
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
request = null;
});
}
那么这里出现了一个有趣的东西,就是 config.cancelToken,和在第二行代码中的 config.cancelToken.promise,这里的代码我们暂时就简单地理解成当执行了请求取消时,请求会被终止,并且 reject。
那现在我们去看 cancel 的内部代码,只有 3 个 js 文件,都在 lib/cancel 文件夹里,然后里面最重要的是 CancelTOken.js 文件,代码不多:
function CancelToken(executor) {
// 省略...
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
那么从这里,我们看到了 this.promise 这个方法,和上面提到的那个其实是同一个,但是这里,它却没有执行 resolve,而是进行了一个变量赋值。而这个 resolve 是要在 executor 函数传入的函数方法执行的时候,才会被执行,并且返回 reason 信息。
那其实到了这里,我们还是得从调用方式反过来推导,到现在 token 和 cancel 方法还没出现呢,请看下面的代码:
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
到这里,就很清晰了,如果调用了 cancel 方法,其实就等同于将上一段代码中的 resolvePromise 给执行了,并且 reason 也会被实例化。然后要注意的是,这里的 token,也就是 config.cancelToken,这里必须是要 cancel 方法执行后,this.promise 才会正式 resolve,不然会一直处于 pending 状态。
那这时候回到 xhr.js 里的那段代码,就真正的连起来了:
执行 cancel 方法 -> 生成 reason 信息 -> promise resolve -> request abort
然后这里还有一点,就是被取消的请求,会在 catch 方法里返回(或者在 then 的第二个方法里返回,但是这个没验证过),是因为在 dispatchRequest 里,在请求准备返回响应时,如果请求被取消了,会 throw 一个错误,而因为都是包在 Promise 里,所以这个异常也是会被捕捉到。
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
而这个 throwIfRequested 方法就是简单地把 reason throw 出来。
# 总结
Axios 其实还有很多接口方法和特性,也是进行了一些抽象封装什么的,不过这些都比较容易看的懂。在看源码的过程中,除了可以发现一些有趣的或者神奇的代码之外,其实重点还是在于学习一些编码的技巧和思路,及项目的管理,例如代码和模块的拆分。