axios 源码的理解

415 阅读22分钟

目录

目录结构

|--lib
   |-- adapters 适配node、browser环境
   |-- cancel 取消请求
   |-- core 核心(实例、拦截器、数据转换、异常...)
   |-- helpers 辅助函数
   |-- axios.js 对外提供的axios函数
   |-- defaults.js 默认配置项
   |-- utils.js 一些工具方法

代码分析(基于 0.19.0 版本)

  • 前言
    • 我们调用的 axios 函数从何而来
    • axios 多种调用方式如何实现
    • createInstance 函数做了什么
  • 配置项
    • 默认配置项
    • 自定义配置项
    • mergeConfig 函数如何处理默认与自定义配置
    • 其它
  • Axios 分析
    • 构造函数
    • request 方法
    • 其它
    • 问答
  • 拦截器管理
    • 拦截器有哪些功能
    • 拦截器的使用
  • 数据处理
    • dispatchRequest 函数
    • transformRequest、transformResponse
  • 取消请求
    • 如何配置cancelToken
    • 内部取消请求的过程
    • 问答
  • 超时
    • 如何配置超时timeout
  • 总结

前言

我们调用的 axios 函数从何而来

//函数调用示例
axios({
  url:'',
  method:'',
  ....
})
// lib/axios.js
// Create the default instance to be exported
var axios = createInstance(defaults);

通过上面源码可以看到变量var axios 接收的是 createInstance 函数执行后的返回值,而我们使用的是axios()函数,说明 createInstance 函数的返回值类型是 Function.

简单理解 axios 就是一个函数.

// 示例代码
function createInstance(config) {
  return function request(config) {
    console.log(config);
  };
}
var axios = createInstance(defaults);

axios 多种调用方式如何实现

//多种调用方式示例
axios.get();
axios.post();
axios.put();

通过以上示例代码我们知道 axios 实际上是一个函数,而在 javascript 中可以直接调用函数的方法,说明该方法是静态方法.

// 静态方法示例代码
var axios = function() {};
//静态方法声明
axios.get = function() {};
axios.post = function() {};
axios.put = function() {};
//静态方法调用
axios.get();

axios 多种调用方式其实就是调用了函数的各种静态方法.

至此我们已经大概了解 axios 函数的由来以及多种调用方式是如何实现的,接下来将深入源码了解 axios 具体的实现.

之前讲过 axios 是 createInstance 函数的返回值,那么我们就从入口开始.

createInstance 函数做了什么

//lib/axios.js
function createInstance(defaultConfig) {
  // 创建Axios实例
  var context = new Axios(defaultConfig);
  // 使用bind生成可执行函数,将request方法在context作用域上执行;
  // instance=function(){};
  var instance = bind(Axios.prototype.request, context);
  // 将Axios.prototype原型上所有的(属性、方法)挂载到instance(函数)上作为静态(方法、属性)供开发者使用;
  // 只有这样开发者才可以直接使用别名方法如axios.get、axios.post;
  // instance.get=Axios.prototype.get;
  // instance.post=Axios.prototype.post
  utils.extend(instance, Axios.prototype, context);

  // 将context上所有的(属性、方法)挂载到instance(函数)上作为静态(方法、属性)供开发者使用;
  // instance.defaults=context.defaults;
  // instance.interceptors=context.interceptors;
  utils.extend(instance, context);
  // 开发者调用的axios其实就是instance这个函数,该函数返回值类型为Promise
  return instance;
}

通过对 createInstance 函数体的分析,大体总结为以下几点:

  • 创建 Axios 实例.
  • 定义 instance 函数(该函数实际上就是 request 方法).
  • instance 函数上挂载 Axios.prototype 原型方法作为静态方法.
  • instance 函数上挂载 context 的属性、方法.

我们将基于这几点进行分析,了解过后会对 axios 有一个整体的理解.

问答

createInstance 函数直接返回 context 是否可以

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  return context;
}

由于 context 是 Axios 实例对象,var axios=context;开发者依然可以使用实例对象上的 get\post\options...等原型上定义的方法,但无法使用 axios({}),因为 createInstance 函数此刻返回的是 context 实例对象而不是一个方法,所以 axios()会直接抛错.

配置项

配置项作为 aixos 的核心之一,贯穿在初始化实例、axios 函数调用、拦截器等所以单独作为一部分讲解.

首先我们看一下默认配置项有哪些.

默认配置项

//lib/defaults.js
var defaults = {
  adapter,
  transformRequest,
  transformResponse,
  timeout,
  xsrfCookieName,
  xsrfHeaderName,
  maxContentLength,
  validateStatus,
  headers: {
    common: {
      Accept,
      delete,
      get,
      head,
      post,
      put,
      patch
    }
  }
};
// lib/axios.js
var axios = createInstance(defaults);

创建 axios 函数时传递的形参便是默认的配置项

默认配置项一般配置常规参数,进而减少开发者对配置项不必要的修改.

自定义配置项

var defaults = {
  baseUrl,
  url,
  method,
  params,
  data
};

自定义配置项通常与业务相关,如 baseUrl、url、data、params 等需要开发者进行配置.

mergeConfig 函数如何处理默认与自定义配置

//lib/core/mergeConfig.js
function mergeConfig(config1, config2){
  ...
}

mergeConfig 函数接收两个参数config1 config2,第一个参数为默认配置项第二个参数为自定义配置项,返回值为合并之后的 config.

在 mergeConfig 函数内主要实现了以下几点操作:

  1. 优先使用开发者自定义配置项-url method params data
var valueFromConfig2Keys = ['url', 'method', 'params', 'data'];

utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
  if (typeof config2[prop] !== 'undefined') {
    config[prop] = config2[prop];
  }
});

以上配置项,如果开发者有配置则优先级最高(直接使用).

  1. 按照优先级顺序(自定义>默认)取值-headers auth proxy
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy'];
utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) {
  if (utils.isObject(config2[prop])) {
    config[prop] = utils.deepMerge(config1[prop], config2[prop]);
  } else if (typeof config2[prop] !== 'undefined') {
    config[prop] = config2[prop];
  } else if (utils.isObject(config1[prop])) {
    config[prop] = utils.deepMerge(config1[prop]);
  } else if (typeof config1[prop] !== 'undefined') {
    config[prop] = config1[prop];
  }
});

以上配置项按照优先级赋值.

  1. 其它默认配置项赋值(自定义>默认)
var defaultToConfig2Keys = [
  'baseURL',
  'url',
  'transformRequest',
  'transformResponse',
  'paramsSerializer',
  'timeout',
  'withCredentials',
  'adapter',
  'responseType',
  'xsrfCookieName',
  'xsrfHeaderName',
  'onUploadProgress',
  'onDownloadProgress',
  'maxContentLength',
  'validateStatus',
  'maxRedirects',
  'httpAgent',
  'httpsAgent',
  'cancelToken',
  'socketPath'
];
utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
  if (typeof config2[prop] !== 'undefined') {
    config[prop] = config2[prop];
  } else if (typeof config1[prop] !== 'undefined') {
    config[prop] = config1[prop];
  }
});

以上对其它默认配置项进行了赋值操作,如果有自定义配置则依然使用开发者自定义配置,否则使用默认.

需要注意的第 2 点中对象类型采用的是utils.deepMerge(config1[prop], config2[prop])合并,将第二个形参、第三个形参...不断的合并,总之是先使用了默认,再使用自定义配置项进行重新赋值,优先级依然是自定义>默认,而第 3 点则直接使用自定义配置项,如果不存在则使用默认(不存在合并)

  1. 开发者自定义属性的处理
var axiosKeys = valueFromConfig2Keys
  .concat(mergeDeepPropertiesKeys)
  .concat(defaultToConfig2Keys);
var otherKeys = Object.keys(config2).filter(function filterAxiosKeys(key) {
  return axiosKeys.indexOf(key) === -1;
});
utils.forEach(otherKeys, function otherKeysDefaultToConfig2(prop) {
  if (typeof config2[prop] !== 'undefined') {
    config[prop] = config2[prop];
  } else if (typeof config1[prop] !== 'undefined') {
    config[prop] = config1[prop];
  }
});

从源码中可以看出变量 axiosKeys 是所有的配置项(默认+自定义),除此之外有可能开发者也会自定义一些私有的配置项,所以第 4 点是对除了 axios 定义的所有配置项之外的私有配置项进行处理.

其它

// axios所有配置项
var axiosKeys = [
  'url',
  'method',
  'params',
  'data',
  'headers',
  'auth',
  'proxy',
  'baseURL',
  'url',
  'transformRequest',
  'transformResponse',
  'paramsSerializer',
  'timeout',
  'withCredentials',
  'adapter',
  'responseType',
  'xsrfCookieName',
  'xsrfHeaderName',
  'onUploadProgress',
  'onDownloadProgress',
  'maxContentLength',
  'validateStatus',
  'maxRedirects',
  'httpAgent',
  'httpsAgent',
  'cancelToken',
  'socketPath'
];

至此所有关于配置项的内容已介绍完总结如下:

  • 按照优先级对配置项赋值(自定义<默认)
  • 对外输出合并之后的配置项

Axios分析

构造函数

// lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  //初始化request、response拦截器实例
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

构造函数内包含了配置项、初始化 request、response 拦截器实例(常用来处理登录、异常消息(后面详细介绍拦截器).

request 方法

Axios.prototype.request = function request(config) {
  /**
   * 如果参数config是字符串类型,则表示开发者使用的是
   * 1.axios(url)
   * 2.axios(url, [config])
   * 这两种方式其中一种方式调用,示例如下:
   * 1.axios('/user/12345')//默认get请求
   * 2.axios('/user',{params:{id:12345}})
   */
  if (typeof config === 'string') {
    //此处处理开发者使用axios(url)这种方式调用,只传递一个参数(url),第二个参数arguments[1]为undefined的情况将config默认为对象(简单理解将参数url合并到config)
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  //合并默认配置项与开发者自定义config(将合并之后的值重新赋值给开发者自定义config)
  config = mergeConfig(this.defaults, config);

  //判断method类型(此处优先级 开发者自定义config>默认配置项this.defaults.method>默认get )
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  var chain = [dispatchRequest, undefined];
  //将config作为参数传入Promise.resolve内,返回一个Promise对象
  var promise = Promise.resolve(config);
  //把request请求拦截器放入chain数组的起始位置
  this.interceptors.request.forEach(function unshiftRequestInterceptors(
    interceptor
  ) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  //把response响应拦截器放入chain数组结束位置
  this.interceptors.response.forEach(function pushResponseInterceptors(
    interceptor
  ) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  //依次循环数组内的request拦截器、dispatchRequest(服务请求)、response响应拦截器
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  //最终返回promise对象
  return promise;
};

其它

//lib/core/Axios.js
utils.forEach(
  ['delete', 'get', 'head', 'options'],
  function forEachMethodNoData(method) {
    Axios.prototype[method] = function(url, config) {
      return this.request(
        utils.merge(config || {}, {
          method: method,
          url: url
        })
      );
    };
  }
);

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(
      utils.merge(config || {}, {
        method: method,
        url: url,
        data: data
      })
    );
  };
});

从源码可以看出对外提供的多种调用方式,其实全部是挂载到原型上的方法,最终统一调用原型上的 request 方法,唯一区别是 post、put、patch 这些方法第二个参数需要传递 data 参数.

问答

构造函数做了哪些

  • this.defaults 接收参数,初始化默认配置项
  • this.interceptors 初始化拦截对象,并且创建 request、response 拦截器的实例对象

构造函数做这两件事情目的是为 Axios.prototype.request 方法做准备.

原型方法 request 中声明 var chain 变量,数组初始化为什么要有 undefined

//lib/core/Axios.js
var chain = [dispatchRequest, undefined];
while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
}

查看 promise.then 的语法,最多可以有 2 个参数.then(onFulfilled[, onRejected])

那么就不难理解 undefined 作为.then 方法的第二个参数用来作为失败的回调. 同理chain.unshift(interceptor.fulfilled, interceptor.rejected)request 请求拦截器和chain.push(interceptor.fulfilled, interceptor.rejected); response 响应拦截器向 chain 添加元素时均是新增两个元素(fulfilled、rejected)处理成功、失败的回调.

//示例代码
var chain = [
  function onFulfilled(value) {
    console.log(value);
  },
  function onRejected(value) {
    console.log(value);
  }
];
var promise = new Promise(function(resolve, reject) {
  resolve('Success');
  //or
  //reject('Error');
});
promise.then(chain[0], chain[1]);

request、response 拦截器为什么分别使用unshift push方法添加元素

//lib/core/Axios.js
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);
});

首先我们需要知道一个流程:发送 request 请求->等待 response 响应,所以拦截器分别在此流程开始之前(request 拦截器)和结束之后(response 拦截器).

request 拦截器->发送 request 请求->等待 response 响应->response 拦截器

按照上面的流程,对应代码

var chain = [dispatchRequest, undefined];

初始化数组已经包含了发送 request 请求->等待 response 响应此步骤(dispatchRequest),按照流程数组内还需要添加request 拦截器response 拦截器

添加 request 拦截器

//lib/core/Axios.js
this.interceptors.request.forEach(function unshiftRequestInterceptors(
  interceptor
) {
  chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

根据流程request 拦截器是在发送 request 请求之前,所以使用unshift在数组起始位置添加元素

unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)

//示例代码
var chain = [3, 4]; //3:请求响应
chain.unshift(1); //1:request拦截请求
//console.log(chain)1,3,4

添加 response 拦截器

//lib/core/Axios.js
this.interceptors.response.forEach(function pushResponseInterceptors(
  interceptor
) {
  chain.push(interceptor.fulfilled, interceptor.rejected);
});

根据流程response 拦截器是在等待 response 响应之后也就是末尾,所以直接使用push在数组末尾位置添加元素

//示例代码
var chain = [1, 3, 4];
chain.push(5); //5:response拦截请求
//console.log(chain)1,3,4,5

源码做的这一系列操作皆是让 变量 chain=[...request 拦截器,请求响应,...response 拦截器] 按照此流程添加元素.

如果在构造函数内只有一个实例对象this.interceptor=new InterceptorManager(), 无法区分请求与响应拦截器(无法实现以上流程步骤),所以this.interceptors对象即有 request 拦截器实例又有 response 拦截器实例的存在.

为什么拦截器是数组类型

无论 request、response 拦截器在实际应用场景中可能负责多个功能.

例如请求拦截器:

  1. request.A token 校验
  2. request.B loading 处理

所以this.interceptors.requestthis.interceptors.response为数组类型.

while 循环如何配合 promise.then 处理流程

使用过 promise 的都知道.then 方法的返回值是 Promise 对象,因此可以使用链式调用. 如果对 Promise 感兴趣,请自行查找相关资料.

//promise.then链式调用示例代码
var promise1 = new Promise(function(resolve, reject) {
  resolve({ url: '/user/12345', method: 'get' });
});
promise1
  .then(function(value) {
    console.log(value); //{url: "/user/12345", method: "get"}
    return value;
  })
  .then(function(value) {
    //如果上一个then方法内没有显示的 return ;则输出undefined
    console.log(value); //{url: "/user/12345", method: "get"}
  });

我们知道了.then 方法可以链式调用,那么 while 循环则是对(流程)数组进行.then 链式调用的具体实现.

//示例代码
var config = {
  url: '/user/12345',
  method: 'get',
  timeout: 3000
};
//request拦截成功回调
function requestOnFulfilled(config) {
  console.log('requestOnFulfilled', config);
  return {...config,status:'requestOnFulfilled'};
}
//request拦截失败回调
function requestOnRejected(reason) {
  console.log('requestOnRejected', reason);
  return Promise.reject(reason);
}
//response拦截成功回调
function responseOnFulfilled(response) {
  console.log('responseOnFulfilled', response);
  return response;
}
//response拦截失败回调
function responseOnRejected(reason) {
  console.log('responseOnRejected', reason);
  return Promise.reject(reason);
}
//请求
function dispatchRequest(config) {
  console.log('dispatchRequest', config);
  return config;
}
var chain = [dispatchRequest, undefined];
chain.unshift(requestOnFulfilled, requestOnRejected);
chain.push(responseOnFulfilled, responseOnRejected);
//chain=[requestOnFulfilled,requestOnRejected,request,undefined,responseOnFulfilled,responseOnRejected];
//将config作为参数传入Promise.resolve内,返回一个Promise对象
var promise = Promise.resolve(config);
while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
}
// while循环分解
// promise
//   .then(requestOnFulfilled, requestOnRejected)
//   .then(dispatchRequest, undefined)
//   .then(responseOnFulfilled, responseOnRejected);

shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度

至此我们已经对 while、promise.then 处理流程有所了解,但需要思考以下几个问题:

  • 为什么循环体内 promise 需要重新赋值promise = promise.then(chain.shift(), chain.shift());而不可以使用promise.then(chain.shift(), chain.shift());
  • var promise = Promise.resolve(config);修改为var promise = Promise.reject(config);requestOnRejected 函数内的return Promise.reject(reason);直接返回 return reason;结果会如何.

config 配置项如何传递到拦截器内

//lib/core/Axios.js
var promise = Promise.resolve(config);

首先我们看创建 Promise 对象的同时把 config 作为参数传递到.resolve 静态方法内,目的就是为了.then 方法的成功回调可以获取到该参数. 简单理解使用.resolve 静态方法创建 Promise 对象时会把参数带入到.then 回调方法内,可查看 Promise 相关资料

//示例代码
var config = {
  url: '/user/12345',
  method: 'get',
  timeout: 3000
};
//request拦截成功回调
function requestOnFulfilled(config) {
  console.log('requestOnFulfilled', config);
  return config;
}
//request拦截失败回调
function requestOnRejected(reason) {
  console.log('requestOnRejected', reason);
  return Promise.reject(reason);
}
var promise = Promise.resolve(config);
var chain = [requestOnFulfilled, requestOnRejected];
//此处只模拟了请求拦截器,所以未使用while循环
promise.then(chain.shift(), chain.shift());

这样一来便可以在请求拦截器内获取到 config,同时也需要注意以下几点:

  • 如果没有配置拦截器,config 是直接传递到了 dispatchRequest 内
//示例代码
var config = {
  url: '/user/12345',
  method: 'get',
  timeout: 3000
};
//请求
function dispatchRequest(config) {
  console.log('dispatchRequest', config);
  return config;
}
var promise = Promise.resolve(config);
var chain = [dispatchRequest, undefined];
promise.then(chain.shift(), chain.shift());
  • 请求拦截器成功回调必须显示的返回 config

如果配置了请求拦截器,需要注意的是在成功回调的函数内必须显示return config,原因在于.then 的链式调用如果没有显示返回,则向下调用的函数内无法获取参数.

//示例代码
var config = {
  url: '/user/12345',
  method: 'get',
  timeout: 3000
};
//request拦截成功回调
function requestOnFulfilled(config) {
  console.log('requestOnFulfilled', config);
  return config;
}
//request拦截失败回调
function requestOnRejected(reason) {
  console.log('requestOnRejected', reason);
  return Promise.reject(reason);
}
//请求
function dispatchRequest(config) {
  console.log('dispatchRequest', config);
  return config;
}
var chain = [dispatchRequest, undefined];
chain.unshift(requestOnFulfilled, requestOnRejected);
var promise = Promise.resolve(config);
while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
}

可以试一下 requestOnFulfilled 函数如果没有return config,dispatchRequest 函数的输出结果是什么.

以上示例代码 dispatchRequest 内返回的是 return config,由于只是简单的示例代码无法演示return response,其实在源码内是要返回 response 对象供向下的响应拦截器使用.

再次强调请求拦截器成功回调必须显示的返回 config 同理响应拦截器成功回调必须显示的返回 response

//lisb/core/dispatchRequest.js
function dispatchRequest(config){
  ...
  return response;
}

以上是对 Axios 做了一个整体的了解,可以总结为以下几点:

  • 构造函数
    • 默认配置项
    • 初始化拦截器
  • request 方法
    • 合并默认配置项与开发者自定义配置项
    • 创建 Promise 对象
    • 整合拦截器、请求(梳理流程)
    • while 循环执行流程
    • 返回 Promise 对象
  • 多种调用方式实现
    • 原型上挂载delete get head options post put patch方法

拦截器管理

拦截器有哪些功能

//lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  ...
};
InterceptorManager.prototype.eject = function eject(id) {
  ...
};
InterceptorManager.prototype.forEach = function forEach(fn) {
  ...
};

查看拦截器的源码只有三个方法,所以接下来对三个方法一一解析.

构造函数

构造函数内比较简单,初始化 handlers 数组用来存储拦截器.

use 添加拦截器

use 方法实现添加拦截器,接收两个参数fulfilled rejected这两个参数实际上作为 Promise 中 resolve 和 reject 的回调,返回值为当前添加的元素在 handlers 内的索引位置,此处返回索引位置作为 ID 便于移除拦截器使用.

eject 移除拦截器

eject 方法实现移动拦截器,接收一个参数id,从 handlers 数组检索并置为 nullthis.handlers[id] = null,之所有这样操作避免索引位置发生变化.

forEach 循环遍历拦截器

循环遍历拦截器数组,接收一个参数fn(Function 类型)在每个拦截器上执行该函数.

拦截器的使用

//示例代码
//request拦截器
axios.interceptors.request.use(
  function(config) {
    // ...
    return config; //显示返回config
  },
  function(reason) {
    return Promise.reject(reason);
  }
);
//response拦截器
axios.interceptors.response.use(
  function(res) {
    // ...
    return res; //显示返回response
  },
  function(reason) {
    return Promise.reject(reason);
  }
);

数据处理

dispatchRequest

在 Axios 分析一节中介绍过该函数,用来执行流程中的发送 request 请求->等待 response 响应,接下来我们看一下具体实现了哪些功能.

//lib/core/dispatchRequest.js
throwIfCancellationRequested(config);
config.data = transformData(
  config.data,
  config.headers,
  config.transformRequest
);
adapter(config).then(function onAdapterResolution(response) {
  response.data = transformData(
    response.data,
    response.headers,
    config.transformResponse
  );
});
  • 是否取消请求 throwIfCancellationRequested函数通过判断 config.cancelToken 是否存在进而取消请求
  • 请求 data 数据处理 在请求之前对 config.data 进行 transformData 数据处理
  • 响应 data 数据处理 adapter.then 响应之后处理 response.data 数据

在请求与响应时都会调用transformData函数,该函数接收三个参数data headers fns,函数体内循环迭代fns的每个函数fn(data, headers),最终返回data

//lib/core/transformData.js
function transformData(data, headers, fns) {
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });

  return data;
}

这就是 axios 中流程 发送 request 请求->等待 response 响应 的具体实现

transformData函数中提到过形参fns,而这个参数传递的实参则是config.transformRequestconfig.transformResponse接下来看一下这两个参数的配置

transformRequest、transformResponse

//lib/defaults.js
transformRequest: [
  function transformRequest(data, headers) {
    return data;
  }
];
transformResponse: [
  function transformResponse(data) {
    return data;
  }
];

在源码中默认配置项transformRequest transformResponse的值是数组类型并且已经配置了默认处理数据的方法,所以即使我们不对这两项进行自定义配置,也依然会对调用默认函数进行数据处理. 那么我们该如何配置?

  • 全局配置 因为初始化 axios 实例时,以 defaults 默认配置作为参数传递到构造函数内(可以看 Axios 分析一节),所以可以直接使用this.defaults进行全局配置
//示例代码
//transformRequest第一种方法 在不覆盖默认配置项的前提下,通过push添加数据处理的方法(可push多项数据处理的方法)
axios.defaults.transformRequest.push(
  function(data, headers) {
    return data;
  },
  (data, headers) => {
    return data;
  }
);
//transformRequest第二种方法 覆盖默认配置项(数组可添加多项数据处理的方法)
axios.defaults.transformRequest = [
  function(data, headers) {
    return data;
  },
  (data, headers) => {
    return data;
  }
];
//transformResponse第一种方法 在不覆盖默认配置项的前提下,通过push添加数据处理的方法
axios.defaults.transformResponse.push(function(data) {
  return data;
});
//transformResponse第二种方法 覆盖默认配置项
axios.defaults.transformResponse = [
  function(data) {
    return data;
  }
];

通过对全局配置,所有 axios 发起的请求都会按照此配置进行数据处理.

  • 局部配置 如果某个请求需要配置数据处理,则可以通过以下局部配置的方式实现
//示例代码
//第一种方法 在不覆盖默认配置项的前提下,post请求配置transformRequest、transformResponse数据处理
axios({
  url: '',
  method: 'post',
  transformRequest: [
    ...axios.defaults.transformRequest,//将默认的配置方法通过扩展运算符拷贝过来
    function(data, headers) {
      ...
      return  data;
    }
  ],
  transformResponse: [
    ...axios.defaults.transformResponse,
    function(data) {
      ...
      return  data;
    }
  ],
});
//第二种方法 直接对transformRequest、transformResponse进行重新定义赋值
axios({
  url: '',
  method: 'post',
  transformRequest: [
    function(data, headers) {
      ...
      return  data;
    }
  ],
  transformResponse: [
    function(data) {
      ...
      return  data;
    }
  ],
});

切记:无论全局配置或局部配置必须要显示的返回值 return data

取消请求

如何配置 cancelToken

之前讲到的配置项一节中有一项cancelToken是对取消请求的配置,有如下两种配置方式.

  1. 通过构造函数创建对象
//示例代码
var cancel;
axios
  .get(url, {
    cancelToken: new axios.CancelToken(c => {
      cancel = c; //参数c则是源码中执行executor方法时对外抛出的函数方法
    })
  })
  .then(res => {
    console.log('请求成功');
  })
  .catch(error => {
    console.log('取消请求');
  });
cancel('取消');
  1. 使用 CancelToken.source 创建
//示例代码
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
var cancel;
axios
  .get(url, {
    cancelToken: cancelToken: source.token
  })
  .then(res => {
    console.log('请求成功');
  })
  .catch(error => {
    console.log('取消请求');
  });
source.cancel('取消');

第二种方式主要是为了方便开发者配置(由之前的第一种方式开发者手动创建对象变为内部创建),source方法内还是通过构造函数创建对象.

//lib/cancel/CancelToken.js
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

以上便是 axios 如何配置取消请求,以第一种配置方式为例看内部是如何操作的.

内部取消请求的过程

//lib/cancel/CancelToken.js
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  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 构造函数内做了什么.
  1. 创建Promise对象,挂载到属性promise上,为下一步取消请求做准备.
  2. 执行executor函数方法——该方法是由开发者通过配置项创建CancelToken对象时传入的参数,类型必须为function.

在执行executor方法时也传递了一个参数cancel,类型为function该参数的存在是为了把对 promise 的控制权交到开发者手中,让开发者控制何时取消请求

这里对外抛出的cancel方法就是上面示例中的c,何时调用c()取消请求完全由开发者控制,方法体内执行resolvePromise方法改变Promise对象的 pending 状态.为后面使用.then 做准备

上面是取消请求的前期准备工作,接下来看何时判断是否取消请求.

  • 判断是否取消请求.
//lib/adapters/xhr.js
if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }
    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

如上面的源码在 xhr.js、http.js 内创建request请求之后,会判断是否配置了config.cancelToken,如果存在则调用promise.then方法内request.abort()终止请求. 这里的config.cancelToken.promise则是上面提到过的 CancelToken 构造函数内做了什么中的第一点 创建Promise对象,挂载到属性promise上,为下一步取消请求做准备.就是为这里终止请求做的准备.

以上就是 axios 取消请求的过程(细品,细细品),大体总结为以下几点:

  1. 开发者配置cancelToken(创建new axios.CancelToken()对象).
  2. CancelToken 构造函数内创建promise对象、执行executor方法对外抛出 cancel 方法 (把取消请求的控制权交给开发者).
  3. 创建发起request请求,判断是否存在cancelToken配置,promise.then内终止请求request.abort().

问答

配置项cancelTokennew axios.CancelToken是从哪里来的.

// lib/axios.js
axios.CancelToken = require('./cancel/CancelToken');

在源码 axios.js 内把CancelToken直接挂载到了 axios 上,所以开发者可以直接创建实例对象new axios.CancelToken.

构造函数内如果没有 Promise 是否可以.

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  // 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);
  });
}
if (config.cancelToken) {
  // Handle cancellation
  // config.cancelToken.promise.then(function onCanceled(cancel) {
  if (!request) {
    return;
  }
  request.abort();
  // reject(cancel);
  // Clean up request
  request = null;
  // });
}
  • 以上是模拟把 Promise 相关的代码注释掉看会发生什么.
  1. 开发者配置cancelToken——没问题.
  2. 构造函数 创建 Promise 对象、执行executor方法对外抛出cancel取消方法(控制权依然交给开发者)——没问题.
  3. 创建发起request请求,判断是否存在cancelToken配置,promise.then内终止请求request.abort()——似乎有问题. 问题出在第 3 点,由于删除了promise的操作,if(config.cancelToken)内直接终止了请求,那这个取消请求的控制权就不在是开发者了(每次发送请求符合if条件便直接终止请求).

为什么Promise决定了取消请求的控制权.

  • 这个是由上面问题的第 3 点延伸出来的问题,我们反推回去看一下,能够执行config.cancelToken.promise.then方法的前提条件一定是需要 promise 对象和改变promise初始化状态pending,这里才可以执行.then,那么我们就需要再次回看上面CancelToken 构造函数内做了什么那部分,发现在创建new axios.CancelToken对象时构造函数内部已经创建了 promise 对象,但状态是pending,所以构造函数内继续执行了executor方法并且对外抛出了cancel方法,而cancel方法内部resolvePromise的执行便是用来改变promise.pending状态,方便后面promise.then使用,到现在只需要关注抛出去(抛给开发者)的cancel方法,开发者什么时候调用,就什么时候改变promise.pending状态(其实上面已经介绍过内部执行流程)

超时

如何配置超时 timeout

请求超时即可以配置全局也可以配置某次请求超时时间,如下 2 个示例.

  1. 全局配置
// 示例代码
axios.defaults.timeout = 10000;
  1. 某次请求配置
// 示例代码
axios.get(url, {
  timeout: 20000
});

如果即配置了全局又配置了某次请求超时的时间,则会优先使用该次请求配置,可参考配置项一节关于优先级的说明.

// lib/adapters/xhr.js
// Set the request timeout in MS
request.timeout = config.timeout;
request.ontimeout = function handleTimeout() {
  var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
  if (config.timeoutErrorMessage) {
    timeoutErrorMessage = config.timeoutErrorMessage;
  }
  reject(createError(timeoutErrorMessage, config, 'ECONNABORTED', request));

  // Clean up request
  request = null;
};

上面是源码关于对请求超时的处理,相对比较简单.

总结

以上章节就是目前对 axios 的理解,还望多多指教.