Axios 源码分析(三): Axios 核心代码概览

本文首发于我的个人博客

本文参加了每周一起学习200行源码共读活动

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

Axios 的核心代码位于 lib/core/Axios

首先什么是核心代码?我理解的是通过一定的逻辑,将工程中的各个模块按照一定流程的进行组合暴露给外部使用的,就是核心代码,说白了就是没它不行。

根据第一篇文章中的流程图,我们可以看到,对外暴露的主函数是在 lib/axioslib/axios 中主要是 继承lib/core/AxiosAxiosAxios 内部就是整个 axios 库的核心操作:发起请求的前后处理。

整个 Axios 采用了 ES5 的写法,使用 function 构建了一个类,之后将各个函数挂载在主函数 Axios 的 prototype 上面。具体的函数关系,可以参考下图。

Axios 核心方法概览.png

看完图,我们接下来看代码。

Axios 主函数

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
复制代码

主函数很简单,就做了两个事:

  • 初始化配置信息 this.defaults,这个其实就是我们不管是通过 axios.create(config) 创建实例后请求,还是直接使用 axios(url, config) 还是 axios(config) 来请求 ( 包含 axios.request() 等 ),或者是使用 axios.defaults 来设置默认配置,所用到的 config 都和 这个 this.defaults 相关,具体的我们一会看。
  • 初始化拦截器 this.interceptors,在这里其实拦截器是 request 和 response 分别初始化了两个 InterceptorManager 实例,InterceptorManager 实例具体是如何实现的,内部又有哪些操作,暂且按下不表,这个是我们下一篇的内容,即使不了解 InterceptorManager 也不会影响对整个 request 的理解。(稍微剧透下,InterceptorManager 内部的基础数据结构是一个数组)

request 请求主流程

乍一看,整个 request 函数 有点多,但其实它只做了两件事:config 处理及 构建 Promise 链 ( 包含请求发送 )。

扫描二维码关注公众号,回复: 13699730 查看本文章

config 处理

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(configOrUrl, config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
    
  // * 此注释为博主自行标注: 第一步
  if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }

  // * 此注释为博主自行标注: 第二步
  config = mergeConfig(this.defaults, config);

  // 此注释为博主自行标注: 第三步
  // * Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  // * 此注释为博主自行标注: 第四步  
  var transitional = config.transitional;

  if (transitional !== undefined) {
    validator.assertOptions(transitional, {
      silentJSONParsing: validators.transitional(validators.boolean),
      forcedJSONParsing: validators.transitional(validators.boolean),
      clarifyTimeoutError: validators.transitional(validators.boolean)
    }, false);
  }
    
  ...
}  
复制代码

整个代码的前三分之一主要是对 config 进行处理。

第一步是判断到底是按 request(url, config) 还是按 request(config) 的方式传入,从而统一为 request 内部使用的 config 方式,即 url 包含在 config 内部。

第二步是将之前你设置过的也好,axios 默认的也好,都和你此次传入的进行合并,主要用到了 mergeConfig ,关于 mergeConfig 的具体实现,我们会在后续的章节中说到。

第三步是确认使用了什么样的请求方式,可以看到,axios 在这里设置了默认的请求方式 get

第四步主要是做了兼容性验证处理,这一块也是以后会有单独的章节。

构建 Promise 链

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(configOrUrl, config) {
  ...
  
  // filter out skipped interceptors
  // * 此注释为博主自行标注: 第一步  
  var requestInterceptorChain = [];
  var synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }

    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;

  // * 此注释为博主自行标注: 第二步-1  
  if (!synchronousRequestInterceptors) {
    var chain = [dispatchRequest, undefined];

    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    chain = chain.concat(responseInterceptorChain);

    promise = Promise.resolve(config);
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
  }


  // * 此注释为博主自行标注: 第二步-2
  var newConfig = config;
    
  // * 此注释为博主自行标注: 第二步-2.1
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected(error);
      break;
    }
  }

  // * 此注释为博主自行标注: 第二步-2.2
  try {
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }
    
  // * 此注释为博主自行标注: 第二步-2.3
  while (responseInterceptorChain.length) {
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
  }

  return promise;
}  
复制代码

第一步是预构建了两条链:请求拦截器链和相应拦截器链。通过调用 InterceptorManager 实例的 forEach 方法来遍历所有设置好的拦截器

第二步是有两个分支,这两个分支主要是根据当时设置拦截器函数时,是否设置了 synchronous 来判断,即axios.interceptors.request.use(fulfilled,rejected,options),中在 options 里设置 synchronous

  • 如果有一个拦截器的 options 不存在 synchronous ,则直接构建一个 Promise 链,链上分别是:请求拦截器、请求派发、响应拦截器。
  • 如果所有拦截器的 options 中都存在 synchronous , 代表你想要同步执行请求拦截器,则将整个请求拆分为三步执行:同步执行请求拦截器、同步发送请求并使用 promise 包装响应、通过前一步的 Promise 响应构建 Promise 响应链并执行。

一般情况下,我们走的都是第一个分支,也就是 if (!synchronousRequestInterceptors)

在构建 Promise 链的过程中,有几处需要我们注意下:

  • 不论请求拦截器还是响应拦截器都是一对一对的。这是为了贴合 promise , promise.then() 可以放入两个函数,第一个是正常执行后调用的函数,第二个是异常执行后调用的函数。正好和拦截器的分配是一样的,都是一对( 正常 + 异常 )。

  • var chain = [dispatchRequest, undefined] 也是为了凑齐一整对,dispatchRequest 是请求派发的方法,它的入参和请求拦截器是一致的,即 config 在经过所有的请求拦截器处理之后,就会送入 dispatchRequest 中进行解析并决定使用什么样的方式去派发请求。这也是为什么请求 url 最后也要放在 config 中的原因。

  • 我们在调试 axios 的拦截器时会发现,请求拦截器是按照拦截器加载的倒序执行,响应拦截器是按照拦截器加载的正序执行。这一块我们可以从上面 requestInterceptorChain 数组构建中发现端倪。

    • requestInterceptorChain 是使用 unshift,也就是每一对拦截器都是直接插入整个 requestInterceptorChain 的头部。而 responseInterceptorChain 是使用的 push,也就是每一对拦截器都是直接插入整个 responseInterceptorChain 的尾部。

    • 所以,你会发现请求拦截器和响应拦截器的执行顺序正好是相反的。

getUri

Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  var fullPath = buildFullPath(config.baseURL, config.url);
  return buildURL(fullPath, config.params, config.paramsSerializer);
};
复制代码

这个方法就很简单,其实就是将 params 传给 paramsSerializer 进行处理,并对 baseURLurl、处理后的 params 进行了字符串拼接。

常用的请求 method 方法

// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
复制代码

我们在日常使用中,不仅仅可以直接通过 axios() 的方式发送请求,也可以通过 axios.get() 这种方式发送请求。

像是 getput 这种别名方法就是通过上面的代码实现的。

挂载时它分成了两种挂载方式:

  • 不可携带 data 的:从 config 中获取 data
  • 可以携带 data 的:设置第二个传参就是 data

当然这里的不可携带 data,并不是真的不给你携带,只是可以传,但没必要。

到此,整个 Axios 核心代码就基本了解完了。当然我们常用的还有 cancelaxios.all 等相关方法并不在核心代码中,这些我们都会在后续的文章中一一拆解。(话说 axios.all 感觉都没必要去分析,它就是个 Promise.all 啊。。。)

猜你喜欢

转载自juejin.im/post/7074982128798138376