记一次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错误
});
复制代码

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

查看所有标签

猜你喜欢:

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

JavaScript实战手册

JavaScript实战手册

David Sawyer McFarland / 李强 / 机械工业出版社 / 2009 / 89.00元

在《JavaScript实战手册》中,畅销书作者David McFarland教你如何以高级的方式使用JavaScript,即便你只有很少或者没有编程经验。一旦掌握了这种语言的结构和术语,你将学习如何使用高级的JavaScript工具来快速为站点添加有用的交互,而不是一切从头开始编写脚本。和其他的Missing Manuals图书不同,《JavaScript实战手册》清楚、精炼,手把手地讲解。 ......一起来看看 《JavaScript实战手册》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具