记一次axios源码排查
栏目: JavaScript · 发布时间: 6年前
内容简介:现在社区中有数量庞大的ajax(http)库,为何选择使用axios呢?首先,因为它提供的API是Promise式的,目前业务代码基本都已经使用async/await来包裹异步api了。那为何不使用基于fetch的类库呢?
现在社区中有数量庞大的ajax(http)库,为何选择使用axios呢?
首先,因为它提供的API是Promise式的,目前业务代码基本都已经使用async/await来包裹异步api了。
那为何不使用基于fetch的类库呢?
因为,选用axios更重要的原因是,需要用到请求的abort。
abort
大部分场景中如果后端处理开销不大,前端使用类似Promise.race或标记位等方式都可以实现前端业务逻辑中的abort。但是如果该请求是一个非常重型的,对数据库读写有压力的请求时,一个实实在在的abort还是有必要的。
当然,可以在后端接口上,设计为创建任务、执行任务、取消任务这样的模式。
由于目前fetch没有abort方式(AbortController目前尚在实验阶段),所以只能使用XMLHttpRequest类来实现具备abort能力的ajax。
二、为何解读?
axios提供了cancel:
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(); 复制代码
实际业务代码示意:
axios({ method: 'get', url: '***', }).then(response => { // 业务逻辑 }).catch(err => { if (axios.isCancel(err)) { // 取消请求 } else { // 业务逻辑错误 } }) 复制代码
期望的结果是,当cancel后,会在业务代码的catch中捕获一个Cancel类型的错误。但实际使用中,该cancelError并没有触发,而是进入了response相关的业务逻辑。
于是,开始了一波debug。一开始怀疑是axios的坑,但当我打开github,看到该项目**4.8万+**的star数时,我确信:
一定是业务代码用错了!
三、代码
1. 文件结构
没有全部细看,把主流程的js看了一遍。
axios/lib │ └───adpaters │ │ ... ajax/http类的封装 │ └───cancel │ │ ... 取消请求的相关代码 │ └───core │ │ │ └───Axios.js 核心类,其余方法没细看 │ └───helpers │ │ ... 工具函数集,没看 │ └───axios.js 入口文件,实例化了核心类 │ └───defaults.js 默认配置 复制代码
2. 主流程
请求发起 | ▼ +----------+ | req中间件 | axios称之为request interceptors +----------+ | ▼ +----------+ | dispatch | 发起请求,内部包含了一些入参转化逻辑,不展开 +----------+ | ▼ +----------+ | Adapter | 适配器,根据环境决定使用http还是xhr模块 +----------+ | ▼ +----------+ | res中间件 | axios称之为response interceptors +----------+ | ▼ +----------+ |transform | 返回值进行一次转换 +----------+ | ▼ 请求结束 复制代码
3. 中间件
axios可以通过axios.interceptors来扩展request/response的中间件:
// Add a request interceptor axios.interceptors.request.use(function (config) { // Do something before request is sent return config; }, function (error) { // Do something with request error return Promise.reject(error); }); // Add a response interceptor axios.interceptors.response.use(function (response) { // Do something with response data return response; }, function (error) { // Do something with response error return Promise.reject(error); }); 复制代码
最后排查结果是某一个中间件出了问题导致的bug,下文再详细展开,先聚焦在中间件相关的源码上:
// core/Axios.js 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()); } 复制代码
核心代码不长,它的目的是,转换出一个Promise数组:
[ ReqInterceptor_1_success, ReqInterceptor_1_error, ReqInterceptor_2_success, ReqInterceptor_2_error, ..., dispatchRequest, undefined, ResInterceptor_1_success, ResInterceptor_1_error, ..., ] 复制代码
再将该数组转换为链式的Promise:
return Promise.resolve( config, ).then( ReqInterceptor_1_success, ReqInterceptor_1_error, ).then( ReqInterceptor_2_success, ReqInterceptor_2_error, ).then( dispatchRequest, undefined, ).then( ResInterceptor_1_success, ResInterceptor_1_error, ) 复制代码
4. 请求取消
先贴一下主要源码:
// 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) { // Cancellation has already been requested return; } token.reason = new Cancel(message); resolvePromise(token.reason); }); } 复制代码
这是CancelToken类的构造函数,它的入参需要是一个函数,该函数的第一个入参会返回 cancel(message) => void
函数,该函数的作用是给CancelToken实例添加一个CancelError类型的reason属性。
axios有两个时机来取消请求。
第一种,在dispatchRequest方法中,在发起请求之前,如果cancel函数执行, throwIfCancellationRequested
会直接把 cancelToken.reason
抛出。
// core/dispatchRequest.js function dispatchRequest(config) { throwIfCancellationRequested(config); // ... } 复制代码
官网示例中的cancel示例就是这第一种取消方式。实际上,请求并没有在调用诸如axios.get方法时立刻发出,而是在microtask中执行(Event Loop相关文档可查阅此处)。具体源码参看上文中间件部分,即使没有任何request中间件,请求也是在 Promise.resolve(config)
的后续中触发。
第二种,在请求发出以后,如果cancel函数执行,在实际的xhr模块中会触发abort。
// adapters/xhr.js config.cancelToken.promise.then(function onCanceled(cancel) { // 此处then会在CancelToken的resolvePromise执行后触发 request.abort(); reject(cancel); }); 复制代码
四、问题排查
1. 大致思路
确认源码以后,CancelError理论上都会被正确throw,并没有犯比较低级的 return new Error('*')
问题。(可以想想为什么~)
既然如此,Error被抛出,那就一定是半路被捕获了。
那最有可能的原因是中间件出了问题,把CancelError给吞了。
2. 真相
最后确认,的确是有一个responseInterceptor:
axiosInstance.interceptors.response.use((resp: AxiosResponse) => { // }, (error: AxiosError): void => { onResponseError(error); }); // 而onResponseError是一个空方法 function onResponseError() {}; 这会导致整个Promise链路变为: Promise.resolve().then(() => { return dispatch(); }) // response中间件 .then(data => { return transform(data); }, err => { catchError(err); // 1. 没有继续抛出错误 }).then(data => { // 2. 错误被中间件捕获后,进入后续resolved逻辑 }).catch(err => { // 3. 无法捕获cancel错误 }); 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。