记一次axios源码排查

栏目: JavaScript · 发布时间: 7年前

内容简介:现在社区中有数量庞大的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错误
});
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java性能权威指南

Java性能权威指南

奥克斯 (Scott Oaks) / 柳飞、陆明刚、臧秀涛 / 人民邮电出版社 / 2016-3-1 / CNY 79.00

市面上介绍Java的书有很多,但专注于Java性能的并不多,能游刃有余地展示Java性能优化难点的更是凤毛麟角,本书即是其中之一。通过使用JVM和Java平台,以及Java语言和应用程序接口,本书详尽讲解了Java性能调优的相关知识,帮助读者深入理解Java平台性能的各个方面,最终使程序如虎添翼。 通过阅读本书,你可以: 运用四个基本原则最大程度地提升性能测试的效果 使用JDK中......一起来看看 《Java性能权威指南》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器