likes
comments
collection
share

axios.get()到底做了什么?

作者站长头像
站长
· 阅读数 53
axios.get()到底做了什么?

前言

相信大家在开发过程中,都使用过 axios 来发送网络请求,那在发送一个网络请求的时候,axios 为我们做了哪些事情呢?本文主要以发送 get 请求为例子,来深入解析一下 axios 的源码。

当然了,本文不会解析 axios 所有功能的实现,只会涉及到一些日常开发中比较常用的几个功能:

  1. 发送 get 请求的两种方式,axios({method: 'get', url: '/api'})axios('/api', {method: 'get'})) 和 axios.get('/api')的实现。
  2. 请求拦截器和相应拦截器的实现。
  3. 取消请求的实现

源码准备

我们先从 axios 的 GitHub 仓库 中克隆源码到本地,接着在终端运行命令 npm install 安装依赖,打开 package.json,找到 main 字段:

axios.get()到底做了什么?

我们可以得知 axios 的入口文件是根目录下的 index.js,打开 index.js 文件,发现它引入了 lib 目录下的 axios.js 文件:

axios.get()到底做了什么?

所以,源码开始的地方就是 lib 目录下的 axios.js 文件。接下来就开始在源码中的海洋里开始畅游啦~

源码解析

lib/axios.js 文件中,第一行代码和第二行代码中分别引入了 utils.js 文件和 ./helpers/bind.js 文件,utils.js 文件里面包含了 51 个工具类的函数,./helpers/bind.js 文件是对 Function.prototype.bind() 方法的实现, 下面来逐一解析本文将会用到的 utils 工具函数和 Function.prototype.bind() 的实现。

Function.prototype.bind() 方法的实现

函数的 bind() 方法返回的是一个新函数,该函数的 this 指向是 bind() 方法传入的第一个参数,./helpers/bind.js 文件代码实现其实就五行,主要是通过函数的 apply() 方法实现。

export default function bind(fn, thisArg) {
  return function wrap() {
    return fn.apply(thisArg, arguments);
  };
}

这里实现的 bind 函数和函数的 bind() 方法的调用方式有点不同,bind 函数的调用方式就跟我们平时调用函数一样,只不过这里的第一个参数是要改变 this 指向的函数,第二个参数才是 this 指向,最后也是返回一个函数。

utils 工具函数

findKey

findKey 函数用来查找对象上是否存在某个属性名的小写,如果存在,那么直接返回它,否则返回 null

const obj = {NaMe: 'jack'};
const originKey = findKey(obj, 'name');
console.log(originKey); // 'NaMe'

findKey 函数有两个参数:

  1. 要查找属性名的对象
  2. 属性名的小写

返回值是对象上的属性名或者 null

代码实现如下:

function findKey(obj, key) {
  key = key.toLowerCase();  // 转换为小写
  const keys = Object.keys(obj);  // 获取对象的属性名
  let i = keys.length;
  let _key;
  
  // 循环遍历查找是否存在某个属性名的小写和 key 一样
  while (i-- > 0) {
    _key = keys[i];
    if (key === _key.toLowerCase()) {
      return _key;
    }
  }
  return null;
}

isContextDefined

isContextDefined 函数用于判断一个对象是否定义了并且不是全局对象,比如在浏览器环境下:

isContextDefined(window); // false,因为 window 是全局对象
let obj = {};
isContextDefined(obj); // true

isContextDefined 函数的参数只有一个,那就是要判断的对象。

代码实现:

const _global = (() => {
  if (typeof globalThis !== "undefined") return globalThis;
  return typeof self !== "undefined" ? self : (typeof window !== 'undefined' ? window : global)
})();

const isContextDefined = (context) => !isUndefined(context) && context !== _global;

isArray

isArray 函数用于判断变量是否为数组

isArray([]);  // true
isArray({});  // false

代码实现:

const {isArray} = Array;

kindOf

kindOf 函数用能够精确地获取所有变量的数据类型

kindOf('a')  // string
kindOf(1)  // number
kindOf(true)  // boolean
kindOf([])  // array

代码实现:

const { toString } = Object.prototype;
const kindOf = (cache => thing => {
    const str = toString.call(thing);
    // cache对象用来缓存计算过的值
    return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
})(Object.create(null));

本质上,kindOf 函数是通过 Object.prototype.toString.call() 获取的变量类型。

kindOfTest

kindOf 函数的二次封装,用来创建判断某个变量是否为指定类型的函数。

const isNumber = kindOfTest('Number');
isNumber(1) // true
isNumber("1") // false

代码实现:

const kindOfTest = (type) => {
  type = type.toLowerCase();
  return (thing) => kindOf(thing) === type
}

isPlainObject

isPlainObject 函数用于判断变量是否为普通的对象。

isPlainObject({});  // true
isPlainObject(new Object());  // true
isPlainObject(new Array());  // false

代码实现:

const { getPrototypeOf } = Object;
const isPlainObject = (val) => {
  if (kindOf(val) !== 'object') {
    return false;
  }
  // 获取变量的原型对象
  const prototype = getPrototypeOf(val);
  return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in val) && !(Symbol.iterator in val);
}

主要的实现思路是通过对象的原型、Symbol.toStringTagSymbol.iterator 不存在普通对象上的条件来做判断的。这里稍微注意一下,如果对象的原型是 null 的话,则是通过 Object.create(null) 方法创建的对象,也属于普通对象。

typeOfTest

用于判断某个变量的类型。接收一个参数,该参数是 typeof 的返回值,返回值是一个函数,该函数用来判断某个变量的类型是否是 typeofTest 函数传入的参数值。

typeOfTest('string')('a') // true

代码实现:

const typeOfTest = type => thing => typeof thing === type;

isFunction

判断变量是否是函数类型。

const noop = () => {}
isFunction(noop) // true

代码实现:

const isFunction = typeOfTest('function');

主要是对 typeofTest 函数的二次封装。

forEach

forEach 函数可以用来遍历数组和对象,在设计模式中称为迭代器模式。

forEach({a: 1, b: 2, c: 3}, function(val, key, obj) {
  // 1, a, {a: 1, b: 2, c: 3}
  // 2, b, {a: 1, b: 2, c: 3}
  // 3, c, {a: 1, b: 2, c: 3}
  console.log(val, key, obj);
})

forEach(['a', 'b', 'c'], function(val, index, arr) {
  // 'a', 0, [1, 2, 3]
  // 'b', 1, [1, 2, 3]
  // 'c', 2, [1, 2, 3]
  console.log(val, index, arr);
})

forEach 函数有两个必选参数和一个可选参数:

  1. 第一个参数代表要遍历的对象或数组
  2. 第二个参数遍历每一个属性或元素时调用的函数,该函数有三个参数,分别代表属性值或元素值,属性名或索引名,对象或数组本身。
  3. 第三个可选参数是一个对象,其属性只有一个 — allOwnKeys,代表是否获取对象自身的所有属性(包括不可枚举的属性)。

代码实现如下:

/**
 * 遍历一个数组或一个对象,遍历每个元素时,都会调用一个函数
 * @param {Object|Array} obj 要遍历的对象
 * @param {Function} fn 遍历每个属性或元素时调用的函数
 *
 * @param {Boolean} [allOwnKeys] 是否获取对象自身的所有属性(包括不可枚举的属性)
 */
function forEach(obj, fn, {allOwnKeys = false} = {}) {
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  let i; // 索引
  let l; // 长度

  if (typeof obj !== 'object') {
    // 如果不是对象,就将它变为数组
    obj = [obj];
  }
  
  // 遍历每个属性或元素,并调用传入的回调函数 — fn
  if (isArray(obj)) {
    for (i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // 获取对象的属性名
    const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
    const len = keys.length;
    let key;

    for (i = 0; i < len; i++) {
      key = keys[i];
      fn.call(null, obj[key], key, obj);
    }
  }
}

注意,如果传入的遍历对象是原始数据类型或者是函数类型,那么会将其放到一个数组中,再进行遍历。

extend

通过向对象 a 添加对象 b 的属性来扩展对象 a

const a = {name: 'jack'};
const b = {age: 18};
extend(a, b, null);
console.log(a); // {name: 'jack', age: 18}

extend 函数有三个必选参数和一个可选参数:

  1. 第一个参数是要扩展的对象。
  2. 第二个参数是要从中添加属性到扩展对象的对象。
  3. 第三个参数是 this 指向,用改变给第二个参数对象中的方法的 this 指向。
  4. 第四个可选参数和 forEach 的第三个可选参数的意义一样。

代码实现:

/**
 * 通过向对象 a 添加对象 b 的属性来扩展对象 a。
 *
 * @param {Object} a 要扩展的对象
 * @param {Object} b 要从中添加属性到扩展对象的对象
 * @param {Object} this 指向,用改变给第二个参数对象中的方法的 this 指向
 *
 * @param {Boolean} [allOwnKeys] 是否获取对象自身的所有属性(包括不可枚举的属性)
 * @returns {Object} 对象 a
 */
const extend = (a, b, thisArg, {allOwnKeys}= {}) => {
  forEach(b, (val, key) => {
    if (thisArg && isFunction(val)) {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  }, {allOwnKeys});
  return a;
}

代码的实现思路其实就是遍历 b 对象,将 b 对象中属性值一一赋值给 a 对象,如果属性值是函数的话,就调用上文介绍到的 bind 函数来改变 this 指向。

merge

merge 用来合并多个对象的属性。它的功能很像 Object.assign() 方法,区别在于,对于普通对象来说,Object.assign() 方法是浅合并,而 merge 是深合并。

const obj1 = { a: 0, b: { c: 0 } };
const obj2 = Object.assign({}, obj1);
obj1.b.c = 3;
console.log(obj1.b.c);  // 3

const obj3 = { a: 0, b: { c: 0 } };
const obj4 = merge({}, obj1);
obj3.b.c = 3;
console.log(obj4.b.c);  // 0

代码实现:

function merge(/* obj1, obj2, obj3, ... */) {
  const {caseless} = isContextDefined(this) && this || {}; // 是否忽略大小写
  const result = {}; // 合并结果
  
  const assignValue = (val, key) => {
    const targetKey = caseless && findKey(result, key) || key;
    if (isPlainObject(result[targetKey]) && isPlainObject(val)) {
      // 如果属性值都是普通对象的情况下,那么再次调用 merge 函数达到深合并的效果
      result[targetKey] = merge(result[targetKey], val);
    } else if (isPlainObject(val)) {
      // 第一次合并的变量就是对象,也要调用 merge 函数进行深合并
      result[targetKey] = merge({}, val);
    } else if (isArray(val)) {
      // 数组不会进行深合并,仅仅是通过 slice 方法进行浅合并
      result[targetKey] = val.slice();
    } else {
      // 其他对象或原始数据类型直接赋值
      result[targetKey] = val;
    }
  }
  
  // 遍历每个对象来进行属性合并,后一个对象的属性会覆盖前一个对象的属性
  for (let i = 0, l = arguments.length; i < l; i++) {
    arguments[i] && forEach(arguments[i], assignValue);
  }
  return result;
}

最核心的代码逻辑就是 assignValue 函数,里面的四个合并条件我也标明了注释。

axios 实例

在熟悉以上 axios 的工具函数之后,我们就来看看 lib/axios.js 的第一段代码 — 创建 axios 实例。

function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  // 省略代码...
}
// Create the default instance to be exported
const axios = createInstance(defaults);

通过调用 createInstance(defaults) 函数来创建一个 axios 实例,其中 defaults 表示 axios 的默认配置,比如 transformRequesttransformResponsetimeoutxsrfCookieNamexsrfHeaderNamemaxContentLengthmaxBodyLengthvalidateStatusheaders 等等属性的默认值。

那么 createInstance 函数内部做了什么事情呢?

首先,通过 new 操作符得出 Axios 类的对象,也就是 const context = new Axios(defaultConfig); 这一行代码的作用,那我们顺着来看看 Axios 类内部做了什么事情。

// lib/core/Axios.js

class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = {
      request: new InterceptorManager(), // 请求拦截器
      response: new InterceptorManager() // 响应拦截器
    };
  }
  // 核心方法
  request(configOrUrl, config) {/*...*/}
  // 省略代码...
}
// 省略代码...

可见,Axios 类在 constructor 方法做了两件事:

  1. 保存默认配置
  2. 初始化请求拦截器和响应拦截器(下文会解析如何实现拦截器)

Axios 类还有个 request 公共方法,它是 axios 库最核心的方法,它会在发送请求之前做一些预处理,以及用来发送请求,也就是说,不管发送任何类型的请求,最后都会调用 request 方法。这个方法会在下文进行详细的解析,目前我们对它有一个大概的了解就行。

Axios 类所处的文件中,你还会发现以下这两段代码:

// lib/core/Axios.js

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

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  function generateHTTPMethod(isForm) {
    return function httpMethod(url, data, config) {
      return this.request(mergeConfig(config || {}, {
        method,
        headers: isForm ? {
          'Content-Type': 'multipart/form-data'
        } : {},
        url,
        data
      }));
    };
  }

  Axios.prototype[method] = generateHTTPMethod();
  // 省略代码...
});

mergeConfig 函数是用来合并两个配置对象用的,其代码实现位于 lib/core/mergeConfig.js 中,在这里就不重点解析了。它其实是对 utilsmerge 函数的扩展,在代码设计模式上称为装饰器模式,在不改变 merge 函数的基础上,通过对其进行包装拓展,使得 mergeConfig 函数可以动态具有更多功能。

上述这两段代码其实就干了一件事:在 Axios 类的原型对象上添加 deletegetheadoptionspostputpatch 方法,并且在内部就是执行了 Axios 类的 request 方法。

Axios 类的原型对象上添加的这些函数为 axios.get() 这种请求方式打下了基石。

得到 Axios 类的对象之后,我们再来看看 createInstance 函数的下一步做了什么:

function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);
  // 省略代码...
  return instance;
}

通过 bind 函数将 Axios 类中的 request 方法赋值给 instance 变量函数,也就是说调用 instance 函数相当于调用 Axios 类中的 request 方法,并且 instance 函数的 this 指向是上一次得出的 Axios 类对象。createInstance 函数最终的返回值就是这个instance 函数。

也就是说 axios 其实是一个函数而不是一个对象。之前说过,无论发送什么类型的请求,最终都会调用 request 方法,所以,这就是为什么在官方文档中可以通过 axios({method: 'get', url: '/api'})axios( '/api', {method: 'get'})) 这种方式发送请求的原因。

axios.get('/api') 这种方式又是怎么实现的呢?别急,我们往下看。

function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);
  
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
  // 省略代码...
  return instance;
}

通过 utils.extend() 方法将 Axios.prototype 对象上的属性值扩展到 instance 函数上,结合之前对 Axios 类的分析,想想这时候 Axios.prototype 对象上有哪些属性?没错,除了request 方法,还有 deletegetheadoptionspostputpatch 属性方法。所以,instance 函数就有了这些属性方法,从而就可以使用 axios.get('/api') 这种方式发送请求。

最后,再来简单说一下 createInstance 函数剩下的两段代码:

function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);

  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
  utils.extend(instance, context, null, {allOwnKeys: true});
  
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

createInstance 函数里面还有最后两段代码,它们主要实现以下这两个功能:

  1. 全局 axios 配置默认值
axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
  1. 自定义实例
// 创建实例时配置默认值
const instance = axios.create({
  baseURL: 'https://api.example.com'
});

全局 axios 配置默认值的功能主要也是通过 utils.extend() 方法将 Axios 类中的实例对象 context 属性值扩展到 instance 函数上,这样就可以通过 axios 来全局配置默认值了。

自定义实例,则是通过给 instance 函数赋值上 create 方法,内部再次调用 createInstance 函数获得另一个 instance 函数。这种方式在设计模式上称为工厂模式,根据不同的输入返回不同的值。

解析完 axios 实例都做了什么事情之后,我们接着来看看拦截器是如何实现的。

InterceptorManager 拦截器

在分析拦截器的源码之前,我们先回顾一下拦截器的使用方法。

添加拦截器

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  }
);

移除拦截器

const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
// eject 方法移除某个拦截器
axios.interceptors.request.eject(myInterceptor);

源码解析

在上文分析 Axios 类时,就提到了在 constructor 方法内初始化了请求和响应拦截器:

class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }
  // 省略代码
}

那现在来看看 InterceptorManager 的源码吧!

源码位于 lib/core/InterceptorManager.js 文件中

class InterceptorManager {
  constructor() {
    // 对象数组,每个对象存放了拦截器的 fulfilled 和 rejected 函数
    this.handlers = [];
  }
  // 添加拦截器的 fulfilled 和 rejected 函数
  use(fulfilled, rejected, options) {
    // 将 fulfilled 和 rejected 函数当作对象的属性存入 handlers 数组中
    this.handlers.push({
      fulfilled,
      rejected,
      // synchronous 属性表示拦截器函数是否同步执行
      synchronous: options ? options.synchronous : false, 
      // 省略代码...
    });
    return this.handlers.length - 1;
  }
  // 移除某个拦截器
  eject(id) {
    if (this.handlers[id]) {
      this.handlers[id] = null;
    }
  }
  // 清除拦截器
  clear() {
    if (this.handlers) {
      this.handlers = [];
    }
  }
  // 遍历所有的拦截器
  forEach(fn) {
    utils.forEach(this.handlers, function forEachHandler(h) {
      if (h !== null) {
        fn(h);
      }
    });
  }
}

通过以上源码,我们得出以下信息:

  1. 当使用 axios.interceptors.request.use()axios.interceptors.response.use() 添加拦截器时,并没有执行拦截器,而是将它先存放到 handlers 数组上,那么拦截器什么时候执行呢?依然是在 Axios 类的 request 方法中执行,下文会详细地解析这个方法。
  2. axios.interceptors.request.use()axios.interceptors.response.use() 的返回值也不是什么拦截器的 id,而是这个拦截器在 handlers 数组中的索引。
  3. 移除拦截器的逻辑是根据索引将 handlers 中的拦截器元素设置为 null 值,因此在使用 forEach 方法遍历 handlers 数组时,需要判断该拦截器元素不为空才执行后续的代码。那为什么不使用数组的 splice 方法来删除呢?因为这个方法的时间复杂的是 O(n)nhandlers 数组的长度。而 this.handlers[id] = null 的时间复杂度是 O(1),效率高太多了。
  4. handlers 数组的拦截器元素中的 synchronous 属性表示拦截器函数是否同步执行,true 为同步执行,false 为异步执行,默认值为 false

request 方法

前面说过,Axios 类中的 request 方法是最核心的方法,任何类型的请求方式最终都是调用这个方法,比如 axios.get('/api') 或者 axios({url: '/api', method: 'get'})axios('/api', {method: 'get'})),这时候我们就会有所疑问,那这些方式最终都会调用 request 方法,而传入参数的类型有可能是一个字符串,也有可能是一个普通对象,或者是一个字符串和一个对象,那怎么去做参数的兼容性处理呢?让我们来看看 request 的处理方式。

request(configOrUrl, config) {
  if (typeof configOrUrl === 'string') {
    // 如果第一个参数是字符串
    config = config || {};
    config.url = configOrUrl;
  } else {
    // 如果第一个参数是对象
    config = configOrUrl || {};
  }
  
  // 省略代码...
}

request 的处理方式非常巧妙,其逻辑是:无论如何,都要保持 config 变量的值为对象。如果第一个参数的类型为字符串,那么就将第一参数值当作 config 对象的 url 属性值,否则就把第一个参数值赋值给 config 变量。

接下来,request 方法主要还对 config 对象做了以下预处理:

  1. 合并默认配置和用户自定义配置
  2. 保证 config 对象中 method 属性值是小写,如: get
  3. config 对象中 headers 属性值的 common 属性和 [config.method] 属性合并为一个对象。
  4. 删除 config 对象中 headers 属性值的 delete, get, head, post, put, patch, common 属性,因为它们已经在存在于 config 对象的 method 字段中。
request(configOrUrl, config) {
  if (typeof configOrUrl === 'string') {
    // 如果第一个参数是字符串
    config = config || {};
    config.url = configOrUrl;
  } else {
    // 如果第一个参数是对象
    config = configOrUrl || {};
  }
  
  // 合并默认配置和自定义配置
  config = mergeConfig(this.defaults, config);
  
  // 省略代码...
  
  // 保持 method 属性值为小写
  config.method = (config.method || this.defaults.method || 'get').toLowerCase();
  
  // 将 headers 中的 common 值和 [config.method] 合并为一个对象
  const {headers} = config;
  let contextHeaders;

  contextHeaders = headers && utils.merge(
    headers.common,
    headers[config.method]
  );
  // 删除 'delete', 'get', 'head', 'post', 'put', 'patch', 'common' 属性
  contextHeaders && utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    (method) => {
      delete headers[method];
    }
  );
  // 合并头信息
  config.headers = AxiosHeaders.concat(contextHeaders, headers);
}

处理好 config 对象的属性之后,那么接下来就到了如何执行拦截器的问题了。而在执行拦截器之前,request 方法会将请求拦截器的函数和响应拦截器的函数各保存到一个数组上。

request(configOrUrl, config) {
  // 省略 config 预处理的代码
  
  // 是否同步执行拦截器
  let synchronousRequestInterceptors = true;
  
  // 保存请求拦截器的 fulfilled 函数和 rejected 函数
  const requestInterceptorChain = [];
  // 遍历通过 use 方法添加的拦截器函数,并将这些函数加入到 requestInterceptorChain
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 省略代码...
    
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected) 
  });
  
  // 保存响应拦截器的 fulfilled 函数和 rejected 函数
  const responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
}

这里有两个需要注意的点:

  1. synchronousRequestInterceptors 变量是用来控制拦截器是否同步执行的,默认拦截器是异步执行的(这在上文 InterceptorManager 拦截器源码解析中讲到过)。
  2. requestInterceptorChain 数组中的请求拦截器处理函数的顺序是和用 use 方法添加的处理函数的顺序相反的,也就是说,最后的执行顺序和添加函数的顺序相反,比如:

axios.interceptors.request.use(function (config) {
  console.log("1");
  return config;
}, function (error) {
  console.log("1");
  return Promise.reject(error);
});

axios.interceptors.request.use(function (config) {
  console.log("2");
  return config;
}, function (error) {
  console.log("2");
  return Promise.reject(error);
});

// 先打印 2,再打印 1。

将拦截器的函数保存到数组之后,执行拦截器的逻辑就比较清晰了,无非就是遍历 requestInterceptorChainresponseInterceptorChain 两个数组,执行数组里的每一个元素函数,但执行拦截器的方式有两种,分别是:异步执行和同步执行。

request(configOrUrl, config) {
  // 省略 config 预处理的代码
  // 省略拦截器预处理的代码

  let promise;
  let i = 0;
  let len;
  // 异步执行
  if (!synchronousRequestInterceptors) {
    // dispatchRequest 函数是正式发送 xhr 请求的函数
    const chain = [dispatchRequest.bind(this), undefined];
    // 将请求拦截器放在 xhr 请求函数之前
    chain.unshift.apply(chain, requestInterceptorChain);
    // 将响应拦截器放在 xhr 请求函数之后
    chain.push.apply(chain, responseInterceptorChain);
    
    len = chain.length;
    promise = Promise.resolve(config);

    while (i < len) {
      // 通过 promise then 链的执行方式达到异步执行的效果
      promise = promise.then(chain[i++], chain[i++]);
    }

    return promise;
  }
  
  // 同步执行
  len = requestInterceptorChain.length;
  let newConfig = config;

  i = 0;
  // 同步执行请求拦截器
  while (i < len) {
    const onFulfilled = requestInterceptorChain[i++];
    const onRejected = requestInterceptorChain[i++];
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected.call(this, error);
      break;
    }
  }
  // 执行 xhr 请求
  try {
    promise = dispatchRequest.call(this, newConfig);
  } catch (error) {
    return Promise.reject(error);
  }
  // 执行响应拦截器
  i = 0;
  len = responseInterceptorChain.length;

  while (i < len) {
    promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
  }

  return promise;
}

这里的 dispatchRequest 函数是用来发送 xhr 请求,返回 Promise 对象,可见它需要在请求拦截器之后和响应拦截器之前执行,dispatchRequest 函数会在下文进行解析。

异步执行拦截器是通过 Promisethen 链形式来完成;而同步执行的情况下,是先直接遍历 requestInterceptorChain 数组,执行请求拦截器,然后再执行 dispatchRequest 函数发送 xhr 请求,最后仍然要通过 Promisethen 链形式来执行响应拦截器,因为 dispatchRequest 函数返回的是一个 Promise 对象。

好了,以上就是 request 方法中的源码了。最后,简单总结一下 request 方法都做了什么:

  1. 预处理 config 配置对象 — 对各种请求方式的传参类型做兼容性处理,确保 method 属性为小写值,处理头信息属性。
  2. 将请求拦截器处理函数和响应拦截器处理函数各放在一个数组上。注意,请求拦截器的处理函数的执行顺序跟添加顺序是相反的。
  3. 根据 synchronousRequestInterceptors 变量判断是同步执行拦截器还是异步执行拦截器,默认是异步执行。

dispatchRequest

根据上文我们知道了 dispatchRequest 函数是用来发送 xhr 请求,其源码位于 lib/core/dispatchRequest.js 文件。其实在 dispatchRequest 函数内,还执行了一个操作 — 取消请求,但这个会留到下文解析。除了取消请求,该函数主要做了以下这几件事:

  1. 完善头信息字段 — headers
  2. 执行 transformRequest 函数数组。transformRequest 允许在向服务器发送前,修改请求数据,只能用于 put, postpatch 这几个请求方法。
  3. 设置 Content-Type 的默认值
  4. 获取请求的方式,是发送浏览器的 xhr 请求还是 Node.js 环境中的 http 模块请求。
  5. 发送请求,通过 Promise 对象的 then 方法接收响应信息,并执行 transformResponse 函数数组,它在通过 axios({url: '/api'}) 函数或axios.get({url: '/api'}) 方法的 then 方法获取响应数据之前会执行,允许修改响应数据。
// 如果已经取消请求了,就抛出错误
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }

  if (config.signal && config.signal.aborted) {
    throw new CanceledError(null, config);
  }
}

function dispatchRequest(config) {
  // 检查是否已经取消请求了
  throwIfCancellationRequested(config);
  // 完善头信息字段
  config.headers = AxiosHeaders.from(config.headers);

  // 执行 transformRequest 数组函数
  config.data = transformData.call(
    config,
    config.transformRequest
  );
  // 设置默认的 Content-Type
  if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
    config.headers.setContentType('application/x-www-form-urlencoded', false);
  }
  // 获取请求方式,当前是 xhr 请求,如果是 node 环境下,则是 http 模块请求
  const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
  
  // 发送请求,接收响应信息,执行 transformResponse 函数数组
  return adapter(config).then(function onAdapterResolution(response) {
    // 检查是否已经取消请求了
    throwIfCancellationRequested(config);

    // 执行 transformResponse 函数数组
    response.data = transformData.call(
      config,
      config.transformResponse,
      response
    );
    // 完善响应头信息
    response.headers = AxiosHeaders.from(response.headers);

    return response;
  }, function onAdapterRejection(reason) {
    // 省略代码...
    return Promise.reject(reason);
  });
}

本文不会解析 transformRequesttransformResponse 的源码,感兴趣的朋友可以自行前往 lib/defaults/index.js 文件中阅读对应的源码。

着重解析一下是如何根据代码的运行环境来获取请求方式的,也就是下面这一行代码中 getAdapter 方法的实现逻辑。

const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

其中 defaults.adapter 的值是 ['xhr', 'http']getAdapter 方法的源码位于 lib/adapters/adapters.js 中,我们来看看它具体做了什么

import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';
// xhr 请求和 http 模块请求
const knownAdapters = {
  http: httpAdapter,
  xhr: xhrAdapter
}

// 省略代码

getAdapter: (adapters) => {
  adapters = utils.isArray(adapters) ? adapters : [adapters];

  const {length} = adapters;
  let nameOrAdapter;
  let adapter;

  for (let i = 0; i < length; i++) {
    nameOrAdapter = adapters[i];
    // 默认情况下,从 knownAdapters 对象中获取请求方式
    if((adapter = utils.isString(nameOrAdapter) ? knownAdapters[nameOrAdapter.toLowerCase()] : nameOrAdapter)) {
      break;
    }
  }
  // 省略代码
  return adapter;
},

从源码上看,axios 默认内置了两种情况方式 — xhr 请求和 http 模块请求,axios 也允许用户自定义请求方式,通过配置对象中的 adapter 属性声明即可。那么假设在当前的浏览器环境下,如何获取这两者的其中一个请求方式呢?

那就需要看看 httpAdapterxhrAdapter 的源码是如何实现的了。它们的源码分别位于 lib/adapters/http.jslib/adapters/xhr.js,由于本文是以浏览器环境的前提下为主来解析代码且 httpAdapter 的实现逻辑相对复杂,所以本文也不会进行解析,只需要知道它是怎么获取相应的请求方式就行。

lib/adapters/http.js 中:

// 省略代码...

const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
export default isHttpAdapterSupported && function httpAdapter(config){/*...*/}

可见是通过判断当前环境是否存在 process 变量来决定是否导出 httpAdapter 函数,当前假设的是浏览器环境,所以导出的 false 值。

lib/adapters/xhr.js 中:

// 省略代码...

const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported && function (config) {/*省略代码*/}

通过判断当前环境是否存在 XMLHttpRequest 对象来决定是否导出 xhrAdapter 函数,当前假设是浏览器环境,所以导出的是 xhrAdapter 函数。

所以,在 lib/adapters/adapters.js 中,knownAdapters 对象的值实际为:

const knownAdapters = {
  http: false,
  xhr: xhrAdapter
}

所以getAdapter 方法自然而然地也就获取到了 xhr 请求方式。getAdapter 方法在设计模式中称为适配器模式,将一个对象的接口(方法、属性)转化为用户需要的另一个接口,解决对象之间接口不兼容的问题。

获取到 xhr 请求方式之后,那么接下来就要看看 xhrAdapter 函数是怎么发送 xhr 请求的了。

取消请求与发送请求

发送请求

发送 xhr 请求的源代码位于 lib/adapters/adapters.js。我们仔细回想一下使用原生的 XMLHttpRequest 对象发送请求的步骤:

  1. 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();
  1. 在这个对象上使用 open 方法创建一个 HTTP 请求,参数是请求方法、请求地址、是否异步和用户的认证信息。比如创建一个异步的 get 请求。
xhr.open('get', '/api', true);
  1. 在正式发起请求前,为这个对象添加一些信息和监听函数,比如,通过 setRequestHeader 方法来为请求添加头信息;监听 onreadystatechange 事件,XMLHttpRequest 对象的状态存在 readyState 属性上,当监听到 readyState 变为 4 时,代表服务器返回的数据接收完成,这时判断返回的状态,如果状态为 2xx 或者 304 则代表返回正常,可以通过 response 中的数据来对页面进行更新。
request.setRequestHeader('Connection', 'keep-alive');
xhr.onreadystatechange = function () {
  if(xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText)
  }
};
  1. 最后调用 send 方法来向服务器发起请求,可以传入参数作为发送请求的数据体。
xhr.send();

其实 lib/adapters/adapters.js 的源代码大致上就是围绕这四个步骤去做一些补充,包括设置超时时间,监听 onloadend 事件,监听 onabort 事件,监听 onerror 事件,监听 ontimeout 事件,取消请求等等。

export default isXHRAdapterSupported && function (config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    let requestData = config.data;
    // 头信息
    const requestHeaders = AxiosHeaders.from(config.headers).normalize();
    // 请求期望的响应类型
    const responseType = config.responseType;
    let onCanceled;
    
    // 服务器返回的数据接收完成后,移除所有取消请求函数
    function done() {
      if (config.cancelToken) {
        config.cancelToken.unsubscribe(onCanceled);
      }

      if (config.signal) {
        config.signal.removeEventListener('abort', onCanceled);
      }
    }

    // 省略代码...
    
    // 创建 XMLHttpRequest 对象
    let request = new XMLHttpRequest();

    // 省略代码...
    
    // 获取完整的请求路径
    const fullPath = buildFullPath(config.baseURL, config.url);
    // buildURL 函数将参数添加到请求路径上
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params), true);

    // 设置超时时间
    request.timeout = config.timeout;
    
    // 请求完成后会执行该函数
    function onloadend() {
      if (!request) {
        return;
      }
      // 准备响应头
      const responseHeaders = AxiosHeaders.from(
        'getAllResponseHeaders' in request && request.getAllResponseHeaders()
      );
      // 服务器返回的数据
      const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
        request.responseText : request.response;
      // 响应数据
      const response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
      };
      
      if (response.status === 200 || response.status === 304) {
         resolve(response);
      } else {
        // 返回错误信息
        reject(new AxiosError('Request failed with status code ' + response.status));
      }
      
      done();

      // 清除请求对象
      request = null;
    }

    if ('onloadend' in request) {
      // 如果 onloadend 事件存在 XMLHttpRequest 对象,就监听 onloadend 事件
      request.onloadend = onloadend;
    } else {
      // 监听 onreadystatechange 事件
      request.onreadystatechange = function handleLoad() {
        if (!request || request.readyState !== 4) {
          return;
        }
        // 省略代码...
        
        /**
         * 由于 readystate 事件在 onerror 事件和 ontimeout 事件之前执行,
         * 所以最终需要延迟执行 onloadend 函数。
         */
        setTimeout(onloadend);
      };
    }

    // 监听 onabort 事件
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      // 返回错误信息
      reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));

      // 清除请求对象
      request = null;
    };

    // 监听 onerror 事件
    request.onerror = function handleError() {
      // 返回错误信息
      reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));

      // 清除请求对象
      request = null;
    };

    // 监听 ontimeout 事件
    request.ontimeout = function handleTimeout() {
      let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
      
      // 省略代码...
      
      // 返回错误信息
      reject(new AxiosError(timeoutErrorMessage));

      // 清除请求对象
      request = null;
    };


    // 如果请求体数据为空,移除 Content-Type 请求头信息字段
    requestData === undefined && requestHeaders.setContentType(null);

    // 设置请求头信息
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
        request.setRequestHeader(key, val);
      });
    }
    
    // 省略代码...

    // 取消请求
    if (config.cancelToken || config.signal) {
      // 取消请求函数
      onCanceled = cancel => {
        if (!request) {
          return;
        }
        // 如果取消请求对象不存在,返回错误信息
        reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
        // 取消 xhr 请求
        request.abort();
        request = null;
      };
      // 添加 onCanceled 请求函数,onCanceled 函数的调用时机由外部决定
      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        // fetch API — AbortController 取消请求的方式
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }

    // 省略代码...

    // 发送请求
    request.send(requestData || null);
  });
}

这里关于取消请求的逻辑会在下文马上解析,因为发送请求与取消请求有一定的关联关系,所以需要先在这里展示如何取消请求的逻辑。

取消请求

axios 官方文档中说明了有两种方式可以取消请求,分别是 fetch API 方式 —AbortControllerCancelToken

// AbortController
const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// 取消请求
controller.abort()
// CancelToken
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/foo/bar', {
   cancelToken: source.token
}).then(function(response) {
   //...
});
// 取消请求
source.cancel('Operation canceled by the user.');

AbortController 取消请求的方式在发送请求的源代码中表现得非常简单,只需要对配置对象中的 signal 属性监听 abort 事件即可。

// lib/adapters/xhr.js

// ...

if (config.cancelToken || config.signal) {
  // 取消请求函数
  onCanceled = cancel => {
    if (!request) {
      return;
    }
    // 如果取消请求对象不存在,返回错误信息
    reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
      // 取消 xhr 请求
    request.abort();
    request = null;
  };
    
  // 省略代码...
    
  if (config.signal) {
    // 监听 abort 事件
    config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  }
}
    
// ...

如果 signal 属性已经是 aborted 状态,那么就直接调用 onCanceled 函数,否则,通过外部调用 controller.abort() 方法,触发 abort 事件调用 onCanceled 函数。

如果最终因为某些原因,没有调用 controller.abort() 方法,那么当请求发送成功,并且响应数据接收完成时,会移除 abort 事件的监听。

// lib/adapters/xhr.js

// ...

function done() {
  // 省略代码...

  // 移除 `abort` 事件的监听
  if (config.signal) {
    config.signal.removeEventListener('abort', onCanceled);
  }
}

// ...

CancelToken 的取消方式源代码稍微复杂一点,它主要运用到了设计模式中的观察者模式

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

    let resolvePromise;

    this.promise = new Promise(function promiseExecutor(resolve) {
      resolvePromise = resolve;
    });

    const token = this;

    this.promise.then(cancel => {
      if (!token._listeners) return;

      let i = token._listeners.length;
      // 执行所有订阅者函数
      while (i-- > 0) {
        token._listeners[i](cancel);
      }
      // 清除所有订阅者
      token._listeners = null;
    });

    // 省略代码
 
    // cancel 回调函数用于通知所有订阅者
    executor(function cancel(message, config, request) {
      if (token.reason) {
        // 如果取消信息不为空,说明已经取消过请求
        return;
      }

      token.reason = new CanceledError(message, config, request);
      // 触发上文的 this.promise.then() 中的回调函数
      resolvePromise(token.reason);
    });
  }

  // 如果已经取消请求了,抛出错误
  throwIfRequested() {
    if (this.reason) {
      throw this.reason;
    }
  }

  // 添加订阅者
  subscribe(listener) {
    if (this.reason) {
      /**
       * 如果取消信息不为空,则说明已经取消过,
       * 应该立马通知订阅者,告诉它已经取消过请求了。
       */
      listener(this.reason);
      return;
    }

    if (this._listeners) {
      this._listeners.push(listener);
    } else {
      this._listeners = [listener];
    }
  }

  // 移除订阅者
  unsubscribe(listener) {
    if (!this._listeners) {
      return;
    }
    const index = this._listeners.indexOf(listener);
    if (index !== -1) {
      this._listeners.splice(index, 1);
    }
  }

  /**
   * 返回一个对象,里面包含了 CancelToken 类的实例对象 token 
   * 和通知所有订阅者的函数 cancel 
   */
  static source() {
    let cancel;
    const token = new CancelToken(function executor(c) {
      cancel = c;
    });
    return {
      token,
      cancel
    };
  }
}

在发送请求的源代码中,实现了添加订阅者的步骤:

// lib/adapters/xhr.js

// ...

if (config.cancelToken || config.signal) {
  // 取消请求函数
  onCanceled = cancel => {
    if (!request) {
      return;
    }
    // 如果取消请求对象不存在,返回错误信息
    reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
    // 取消 xhr 请求
    request.abort();
    request = null;
  };
  // 添加订阅者
  config.cancelToken && config.cancelToken.subscribe(onCanceled);
    
  // 省略代码...
}
    
// ...

那什么时候通知所有订阅者呢?还记得一开始举的官方例子吧?其中的 source.cancel('Operation canceled by the user.') 就是用来通知所有订阅者的。之后就会执行 request.abort() 代码,中断 xhr 请求。

同样地,如果最终因为某些原因,没有调用 source.cancel('Operation canceled by the user.') 方法,那么当请求发送成功,并且响应数据接收完成时,会移除订阅者。

// lib/adapters/xhr.js

// ...

function done() {
  // 移除订阅者
  if (config.cancelToken) {
    config.cancelToken.unsubscribe(onCanceled);
  }
  
  // 省略代码...
}

// ...

总结

  1. 无论是用 axios(config)还是用 axios.get(url[, config]) 发送请求,其本质上都是在调用 Axios 类中的 request 方法,request 方法会对各种传参形式做一个统一处理。

  2. 拦截器默认是异步执行的,如果想要同步执行,则要 axios.interceptors.(request|response).use 方法的第三个参数中设置 { synchronous: true }。拦截器函数也是在 Axios 类中的 request 方法的执行。

  3. 本质上,axios 就是 xhr 请求(浏览器环境下)或者 http 模块请求(Node.js 环境下),只不过在正式发送请求之前,做了大量的预处理工作。

  4. 取消请求的方式分为两种:AbortControllerCancelToken,现官方推荐用 AbortController。源代码实现上,AbortController 也比较简单明了,只需要监听 abort 事件即可;而 CancelToken 则是运用了设计模式中观察者模式的思想,通过调用外部的 cancel 方法来通知所有的订阅者,从而执行 request.abort() 方法,完成取消请求。

  5. axios 中运用了大量的设计模式,比如本文提到过的迭代器模式、装饰器模式、适配器模式、工厂模式、观察者模式。如果想系统学习设计模式的话,推荐曾探老师的《JavaScript设计模式与开发实践》。

转载自:https://juejin.cn/post/7246777363256590393
评论
请登录