记一次axios源码排查

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

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

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

查看所有标签

猜你喜欢:

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

Design Accessible Web Sites

Design Accessible Web Sites

Jeremy Sydik / Pragmatic Bookshelf / 2007-11-05 / USD 34.95

It's not a one-browser web anymore. You need to reach audiences that use cell phones, PDAs, game consoles, or other "alternative" browsers, as well as users with disabilities. Legal requirements for a......一起来看看 《Design Accessible Web Sites》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

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

Markdown 在线编辑器