Axios 源码解析【1.3.5】

1,443 阅读22分钟

Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js

在服务端它使用原生 node.js http 模块, 而在浏览器则使用 XMLHttpRequest

它是前端项目中最流行的http请求插件,拥有很多强大的功能:

  • 从浏览器创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据

了解它的基本原理,有利于我们更好的应用封装和扩展。

1,源码目录结构

Axios1.3x的目录结构如下:

├── bin           # 与构建相关的脚本和配置文件
├── dist          # 构建后的文件
├── examples      # 文档案例
├── lib           # axios库源码
│ ├── abapter     # 请求适配器xhr/http
│ ├── cancel      # 取消请求相关
│ ├── core        # Axios实例/请求/拦截器等核心功能源码
│ ├── defaults    # 默认配置
│ ├── env
│ ├── helpers     # 请求工具函数:包括对请求头和响应头的解析、对URL的处理,以及对各种错误的处理等。
│ ├── platform    # 特定平台:浏览器/node
│ ├── axios.js    // 入口文件
│ ├── utils.js    // 全局通用封装方法,导出一个utils工具对象
├── sandbox         # 沙盒模拟请求
├── templates       # 模板文件
├── test            # 测试相关

/lib目录就是axios的插件源码,是我们需要掌握了解的,我们首先从入口文件axios.js开始。

2,axios实例的定义

查看axios.js源码:

// lib/axios.js
​
import utils from './utils.js';
import bind from './helpers/bind.js';
import Axios from './core/Axios.js';
import mergeConfig from './core/mergeConfig.js';
import defaults from './defaults/index.js';
import formDataToJSON from './helpers/formDataToJSON.js';
import CanceledError from './cancel/CanceledError.js';
import CancelToken from './cancel/CancelToken.js';
import isCancel from './cancel/isCancel.js';
import {VERSION} from './env/data.js';
import toFormData from './helpers/toFormData.js';
import AxiosError from './core/AxiosError.js';
import spread from './helpers/spread.js';
import isAxiosError from './helpers/isAxiosError.js';
import AxiosHeaders from "./core/AxiosHeaders.js";
import HttpStatusCode from './helpers/HttpStatusCode.js';# 使用默认配置:创建一个axios实例【wrap函数】,为了导出使用
const axios = createInstance(defaults);# 在axios实例上挂载相关的属性和方法
// 挂载Axios类
axios.Axios = Axios;
​
// 挂载取消请求接口
axios.CanceledError = CanceledError;
axios.CancelToken = CancelToken;
axios.isCancel = isCancel;
axios.VERSION = VERSION;
// 挂载转formData格式方法
axios.toFormData = toFormData;
​
// 挂载错误处理类
axios.AxiosError = AxiosError;
​
// 取消请求兼容处理
axios.Cancel = axios.CanceledError;
​
// 挂载all方法【批量请求】
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = spread;
​
// Expose isAxiosError
axios.isAxiosError = isAxiosError;
​
// 挂载合并config方法
axios.mergeConfig = mergeConfig;axios.AxiosHeaders = AxiosHeaders;
// 挂载转json方法
axios.formToJSON = thing => formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);
// 挂载http状态码对象
axios.HttpStatusCode = HttpStatusCode;axios.default = axios;# 导出axios实例
export default axios

根据上面源码可以看出axios.js入口文件的主要作用就是使用默认配置创建了一个axios实例,并且在实例上挂载了一系列的属性和方法,最后导出axios实例,我们就可以在前端项目中进行引入使用。

# 在项目中引入使用
import axios from 'axios'// 创建请求实例
const service = axios.create({
    baseURL: import.meta.env.VITE_APP_BASE_API,
    timeout: 10000
})
// 注册拦截器
service.interceptors.request.use()
service.interceptors.response.use()
​
export default service

在使用之前,我们先打印一下引入的axios实例,查看它的结构:

import axios from 'axios'
console.log(axios)

1.png

根据打印结果可以发现,axios实例身上挂载属性和方法符合前面的定义。

同时我们我们还注意到了这里的axios实例是一个wrap函数,不是我们想象的普通对象。

我们要了解其中的缘由,还得继续需要深入了解一下:

const axios = createInstance(defaults);
createInstance

查看createInstance方法源码:

# 创建请求实例
function createInstance(defaultConfig) {
  # 根据默认配置,初始化创建了一个axios实例
  const context = new Axios(defaultConfig);
  # 将axios对象实例包装为一个函数实例,
  // 其实就是返回了一个wrap函数,它的内部执行了axios.request方法【所以,这里的wrap函数就是请求实例】
  const instance = bind(Axios.prototype.request, context);
​
  // Copy axios.prototype to instance
  # 将axios对象实例上的内容:全部继承到函数实例身上
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
  // Copy context to instance
  utils.extend(instance, context, null, {allOwnKeys: true});
​
  # 定义实例的create方法,内部为调用的createInstance
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };
  
  # 返回axios函数实例
  return instance;
}

根据上面的代码可以看出,首先根据默认配置对象defaultConfig创建一个axios对象实例【context】,然后使用bind方法创建了真正的axios函数实例【instance】。

这里我们需要查看bind方法源码:

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

【转换一下】这里的instance等同于:

const instance = function wrap() {
    return Axios.prototype.request.apply(Axios, arguments)
}

createInstance方法最后导出的实例是instance,所以axios实例其实就是wrap函数。在调用bind方法后,使用了utils.extend方法,将axios对象实例身上的所有内容复制到了wrap函数身上。所以最终生成的axios实例就是一个wrap函数,符合我们前面打印的axios实例结构。

这里不直接使用axios对象实例,而是转换成函数实例。其实是为了扩展Axios API,让用户可以直接使用axios(config)发起请求。

// 发起一个post请求
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

通过这样的处理,axios可以支持更多方式来发起请求:

  • axios(config)
  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.post(url[, data[, config]])等等...

当然它们的底层原理都是依赖于axios.request(config)方法,后面我们会详细讲解这个方法。

根据前面的解析,我们已经知道了项目中引入的axios实例的由来以及为啥它是一个函数实例。

下面我们再来看看项目中的一些应用:

import axios from 'axios'// 创建axios实例
const service = axios.create({
    baseURL: import.meta.env.VITE_APP_BASE_API,
    timeout: 10000
})
// 定义请求拦截器
service.interceptors.request.use(
    config => {},
    error => {}
)
// 定义响应拦截器
service.interceptors.response.use(
    res => {},
    error => {}
)
​
export default service
// 引入上面导出的axios实例
import axios from '../utils/axios'// 登录
export function login(data) {
    return axios({
        url: '/api/admin/login',
        method: 'post',
        data
    })
}

在项目中:我们可以通过axios实例定义请求和响应拦截器,以及发起真正的http请求。而在前面我们已经知道axios实例【即wrap函数】的属性和方法都是通过继承new Axios()创建的axios对象实例得来的,所以我们要了解拦截器和发起http请求的方法实现,还得回到真正的axios对象中。

const context = new Axios(defaultConfig);
class Axios

查看Axios类的源码:

class Axios {
  constructor(instanceConfig) {
    # 初始化两个属性
    // 存储默认配置对象
    this.defaults = instanceConfig;
    // 创建拦截器对象interceptors
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }
  
  # 发起http请求
  request() {
      ...
      # 返回响应内容
      return promise;
  }
    
  getUri(config) {}
}

Axios类的源码内容并不多,构造器里只是简单的初始化了两个实例属性defaultsinterceptors。这里要注意一下interceptors拦截器对象,它里面有两个属性:request/response,对应着请求拦截器和响应拦截器。然后Axios里面的核心内容是它的request方法了,这个方法的作用是发起真正的http请求以及返回请求成功的响应内容,可以说它是axios插件的核心,它的内容比较多,所以我们后面再讲解这部分内容。

在导出Axios类之前,还做了一些额外的挂载,也需要我们了解一下。

// utils工具对象
import utils from './../utils.js';
​
class Axios {}
​
# 在Axios.prototype原型对象上挂载请求方法
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,
      url,
      data: (config || {}).data
    }));
  };
});
​
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
​
  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();
​
  Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});
​
// 导出Axios类
export default Axios;

utilsaxios仓库中的全局工具对象,它里面封装了很多自定义的方法。

utils.forEach(list, fn)

这里的utils.forEach方法的作用是:循环list参数,将每一项元素作为fn的参数并调用fn

所以这里的作用就是:在导出Axios类之前,执行两个循环,将getpostputdelete等方法都挂载到Axios.prototype原型对象身上,所以我们在项目中就可以通过使用axios.get或者axios.post来便捷的发起http请求。

并且我们可以看出这些方法的内部都是调用的this.request(),即axios.request()。所以在这里我们就可以知道,axios插件中的所有请求方法,比如axios.get()axios.post()这些等等,其内部都是依赖于axios.request方法,它才是真正的http请求方法。

在解析request方法之前,我们还得先看看拦截器interceptors的相关实现。

3,拦截器的定义

class InterceptorManager

查看InterceptorManager类的源码:

# 拦截器class
class InterceptorManager {
  constructor() {
    // 初始化handlers【存储处理器】
    this.handlers = [];
  }
​
  # 向handlers数组中添加handler处理器对象
  use(fulfilled, rejected, options) {
    // 参数:请求成功的钩子,请求失败的钩子,配置对象
    this.handlers.push({
      fulfilled,
      rejected,
      synchronous: options ? options.synchronous : false,
      runWhen: options ? options.runWhen : null
    });
    // 返回处理器索引
    return this.handlers.length - 1;
  }
​
  // 通过索引移除指定的处理器
  eject(id) {
    if (this.handlers[id]) {
      this.handlers[id] = null;
    }
  }
​
  // 清除所有处理器
  clear() {
    if (this.handlers) {
      this.handlers = [];
    }
  }
​
  # 循环处理所有handler【请求时使用】
  forEach(fn) {
    utils.forEach(this.handlers, function forEachHandler(h) {
      if (h !== null) {
        fn(h);
      }
    });
  }
}
​
# 导出拦截器class
export default InterceptorManager;

拦截器InterceptorManager的内容也比较简单,自身只有一个handlers属性,作用是存储handler处理对象。拦截器的所有方法都是围绕着handlers数组进行操作。比如我们常用的use方法:

# 注册请求拦截器
service.interceptors.request.use(
    config => {},
    error => {}
)

use方法的作用:就是将我们传入的成功回调和失败回调包装到一个处理对象中【这样的一个对象可以称为一个处理器】,然后添加到handlers数组中。

this.handlers.push({fulfilled, rejected})

axios中只有两种拦截器:请求拦截器和响应拦截器。

但是在一个拦截器中,我们可以定义多个处理器:

# 给请求拦截器 定义两个处理器
// 【处理器只是根据源码方便称呼,也可以说注册多个请求拦截器】
service.interceptors.request.use(
    config => {},
    error => {}
)
service.interceptors.request.use(
    config => {},
    error => {}
)

这时service.interceptors.request.handlers属性中就会存在两个处理器对象

2.png

4,发起http请求

本节我们开始讲解axios.request()方法。

request

查看request方法源码:

# 发起请求
request(configOrUrl, config) {
  if (typeof configOrUrl === 'string') {
    config = config || {};
    // 存储url
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }
​
  # 合并config,生成本次http请求的配置 【注意:传入的config可以覆盖默认的全局配置】
  config = mergeConfig(this.defaults, config);
​
  const {transitional, paramsSerializer, headers} = config;
​
  if (transitional !== undefined) {
    validator.assertOptions(transitional, {
      silentJSONParsing: validators.transitional(validators.boolean),
      forcedJSONParsing: validators.transitional(validators.boolean),
      clarifyTimeoutError: validators.transitional(validators.boolean)
    }, false);
  }
  
  // 参数处理
  if (paramsSerializer != null) {
    if (utils.isFunction(paramsSerializer)) {
      config.paramsSerializer = {
        serialize: paramsSerializer
      }
    } else {
      validator.assertOptions(paramsSerializer, {
        encode: validators.function,
        serialize: validators.function
      }, true);
    }
  }
​
  # 设置本次请求方式【全部转小写】
  config.method = (config.method || this.defaults.method || 'get').toLowerCase();
​
  let contextHeaders;
​
  // Flatten headers
  # 格式化请求头
  contextHeaders = headers && utils.merge(
    headers.common,
    headers[config.method]
  );
​
  contextHeaders && utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    (method) => {
      delete headers[method];
    }
  );
  # 设置请求头
  config.headers = AxiosHeaders.concat(contextHeaders, headers);
​
  # 重点:处理拦截器
  // 请求拦截器钩子执行链
  const requestInterceptorChain = [];
  // 是否同步执行:请求拦截器
  let synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // runWhen默认null, synchronous默认为false
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }
    # 默认都是异步执行
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    # 将请求拦截器的所有钩子:添加到执行链列表
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
​
  // 响应拦截器钩子执行链
  const responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    # 将响应拦截器的所有钩子:添加到执行链列表
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
​
  let promise;
  let i = 0;
  let len;
    
  # 默认异步执行【走这个分支逻辑】
  if (!synchronousRequestInterceptors) {
    // 创建执行链
    const chain = [dispatchRequest.bind(this), undefined];
    chain.unshift.apply(chain, requestInterceptorChain);
    chain.push.apply(chain, responseInterceptorChain);
    len = chain.length;
​
    promise = Promise.resolve(config);
    # 循环调用钩子函数,顺序是请求钩子 => 发起真正的http请求 => 响应钩子
    while (i < len) {
      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;
    }
  }
​
  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;
}

总结: request方法里面主要有以下几个重点逻辑:

  • 合并config,生成本次http请求的配置。
  • 设置本次请求方式config.method
  • 设置请求头config.headers
  • 处理拦截器,循环执行任务与发起http请求。
  • 返回响应结果。

根据这里的mergeConfig,我们可以印证官网的描述:

【axios官网】配置的优先级:配置将会按优先级进行合并。它的顺序是:在lib/defaults.js中找到的库默认值,然后是实例的 defaults 属性,最后是请求的 config 参数。后面的优先级要高于前面的。

所以我们每一次axios请求传入的配置参数都拥有【最高的优先级】,比如:

export function login(data) {
    return axios({
        url: '/api/admin/login',
        method: 'post',
        data,
        # 会覆盖默认的,以及全局的配置
        timeout: 10000,
        contentType: ''
    })
}

这就是aioxs强大之处,既可以全局封装配置,又可以给独立的请求配置不同的请求参数。

下面我们再继续看拦截器的处理,在分析拦截器之前,我们再回顾一下拦截器的设置:

// 设置拦截器
service.interceptors.request.use(
    config => {},
    error => {}
)
service.interceptors.response.use(
    config => {},
    error => {}
)

拦截器的use方法:

use(fulfilled, rejected, options) {
    // 参数:请求成功的钩子,请求失败的钩子,配置对象
    this.handlers.push({
      fulfilled,
      rejected,
      synchronous: options ? options.synchronous : false,
      runWhen: options ? options.runWhen : null
    });
    return this.handlers.length - 1;
  }

无论是官网案例,还是我们在项目中使用时,定义拦截器都只传递了前面两个钩子函数,所以我们可以得出两个结论:

  • synchronous默认都为false
  • runWhen默认都为null

我们可以打印查看:

3.png

// 是否同步执行请求拦截器
let synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    # runWhen默认null, synchronous默认为false
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }
    # 默认都是异步执行
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    # 将请求拦截器的所有钩子:添加到钩子列表里面
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

所以synchronousRequestInterceptors变量会一直为false,即默认为异步执行请求拦截器。

# 默认异步执行【走这个分支逻辑】
if (!synchronousRequestInterceptors) {
    // 执行链
    const chain = [dispatchRequest.bind(this), undefined];
    // 将请求拦截器的钩子添加到执行链头部
    chain.unshift.apply(chain, requestInterceptorChain);
    // 将响应拦截器的钩子添加到执行链尾部
    chain.push.apply(chain, responseInterceptorChain);
    len = chain.length;
​
    promise = Promise.resolve(config);
    # 循环调用钩子函数,顺序是请求钩子 => 发起真正的http请求 => 响应钩子
    while (i < len) {
      # 将钩子添加到微任务,链式调用
      promise = promise.then(chain[i++], chain[i++]);
    }
    # 返回响应内容
    return promise;
}

所以request方法最终会走这个分支进行拦截器执行与发起http请求。

这里首先定义了一个执行链chain,存入了一个dispatchRequest钩子,然后将请求拦截器的所有钩子添加到执行链头部,将响应拦截器的所有钩子添加到执行链的尾部。

dispatchRequest函数是真正发起http的方法,后面我们会详细分析它的内容。

所以这里的执行链顺序是:

  • 触发请求钩子。
  • 触发http请求。
  • 触发响应钩子。

我们来打印一下结果印证:

4.png

整理好执行链chain之后:

promise = Promise.resolve(config)

使用Promise.resolve创建了一个fulfilled状态的promise对象,并且是将处理好的config配置对象作为resolve参数。

然后开始循环,使用promise.then依次将执行链chain中的钩子添加微任务执行,这里的调用结果又赋值给了promise形成了链式调用,一直到执行完成,返回最后的promise结果。

这里的链式调用非常重要,我们转变一下它的形式进行更好的理解:

# 链式调用
promise.then(requestResolve(), requestReject())
.then(http(), undefined)
.then(responseResolve(), responseReject())

因为这里的promise对象是fulfilled状态,所以在执行then方法是,默认就会执行requestResolve,即请求拦截器的config钩子。根据执行结果,总共可以分为三种场景:

  • requestResolve执行成功,则会发起http请求,请求成功则会进入responseResolve,最终返回响应结果res
  • requestResolve执行成功,则会发起http请求,请求失败则会进入responseReject,返回失败原因error
  • requestResolve执行报错【比如语法错误,设置请求头错误】,则会进入responseReject,返回失败原因error

这就是执行链chain的完整执行过程,即axios发起一个http请求的过程。

拦截器的原理: 就是在axios每次发起http请求之前和请求之后触发一些用户定义的钩子函数。

拦截器的原理我们已经知道了,下面我们开始分析axioshttp具体请求过程。

dispatchRequest

前面我们已经知道了dispatchRequest函数是真正发起http请求的方法,下面开始解析它的源码:

# 发起真正的http请求
export default function dispatchRequest(config) {
  // 发起真正的请求之前:如果取消请求,则抛出CanceledError
  throwIfCancellationRequested(config);
  
  # 继续请求的情况下:
  // 设置请求头
  config.headers = AxiosHeaders.from(config.headers);
​
  // Transform request data
  # 处理请求数据
  config.data = transformData.call(
    config,
    config.transformRequest
  );
  
  // 设置请求内容类型
  if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
    config.headers.setContentType('application/x-www-form-urlencoded', false);
  }
  
  # 获取适配器
  // adapter是axios库中负责发送网络请求的适配器(即执行HTTP请求并返回响应数据的功能实现)
  const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
​
  # 通过适配器发起真正的http请求
  return adapter(config).then(
      // 折叠
      () => { ... },
      () => { ... }
  );
}

dispatchRequest函数的内容可以分为三部分:

  • 发起真正的请求之前,可以调用throwIfCancellationRequested(config)取消请求,抛出CanceledError。
  • 处理请求数据和请求的内容类型contentType
  • 获取适配器adapter
  • 使用适配器adapter发起http请求。
cancelToken

我们首先查看throwIfCancellationRequested取消请求方法:

# 取消请求,抛出CanceledError
function throwIfCancellationRequested(config) {
   # 取消请求有两种方式
  // CancelToken 方式
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
  // AbortController 方式
  if (config.signal && config.signal.aborted) {
    throw new CanceledError(null, config);
  }
}

取消请求有两种方式,使用任何一种都可以。

  • CancelToken
  • AbortController【新版本推荐使用】
const controller = new AbortController();
service({
    method: 'get',
    signal: controller.signal,
    url: 'https://www.fastmock.site/mock/43324a7f3da68215e1834d217d35d1c7/api/userInfo',
    timeout: 5000,
})
// 取消请求
controller.abort()

controller.abort()会让signal.aborted状态为true,就可以在真正发起请求之前,抛出errer,取消请求。

throw new CanceledError(null, config);

5.png

adapter

我们继续查看适配器adapter【非常重要】:

  • 浏览器环境:依赖于XMLHttpRequest,使用xhr对象发起http请求。
  • node环境:依赖于nodejs的原生http模块。
// 获取适配器
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

在项目我们一般不会配置adapter参数,所以这里都会使用默认的配置defaults.adapter

// lib/defaults/index.js
const defaults = {
    adapter: ['xhr', 'http'],
}

默认适配器有xhr对象和http对象两种,axios会根据依赖环境选择其中一个。

继续查看adapters.getAdapter方法:

# adapters对象
export default {
  # 获取适配器
  getAdapter: (adapters) => {
    // 取出适配器列表 ['xhr', 'http']
    adapters = utils.isArray(adapters) ? adapters : [adapters];
​
    const {length} = adapters;
    let nameOrAdapter;
    let adapter;
​
    for (let i = 0; i < length; i++) {
      nameOrAdapter = adapters[i];
      # 在这里判断时:
      // 如果是浏览器环境:第一次循环即可取到xhr请求器,赋值给adapter,然后直接break跳出循环,
      // 如果node环境:第一次循环xhr为null,所以需要第二次循环,取出http请求,赋值给adapter
      if((adapter = utils.isString(nameOrAdapter) ? knownAdapters[nameOrAdapter.toLowerCase()] : nameOrAdapter)) {
        break;
      }
    }
​
    // 如果没有适配器则报出异常提示
    if (!adapter) {
      if (adapter === false) {
        throw new AxiosError(
          `Adapter ${nameOrAdapter} is not supported by the environment`,
          'ERR_NOT_SUPPORT'
        );
      }
      throw new Error(
        utils.hasOwnProp(knownAdapters, nameOrAdapter) ?
          `Adapter '${nameOrAdapter}' is not available in the build` :
          `Unknown adapter '${nameOrAdapter}'`
      );
    }
​
    if (!utils.isFunction(adapter)) {
      throw new TypeError('adapter is not a function');
    }
​
    # 返回适配器
    return adapter;
  },
  adapters: knownAdapters
}

getAdapter方法最重要的作用就是循环传入的adapters列表:

['xhr', 'http']

然后根据环境选择适配器对象adapter,最后返回适配器。

这里在循环判断时:

  • 如果是浏览器环境:第一次循环即可取到xhr对象,赋值给adapter,然后直接break跳出循环。
  • 如果是node环境:第一次循环xhrnull,所以需要第二次循环,取出http请求,赋值给adapter

我们来打印一下印证:

6.png

knownAdapters对象我们还没有提及,它其实存储的就是适配器:

import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';
// 存储适配器
const knownAdapters = {
  http: httpAdapter,
  xhr: xhrAdapter
}

我们来分别看一下这两个适配器:

  • xhrAdapter
// lib/adapters/xhr.jsconst isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported && function (config) {}

在浏览器环境下存在XMLHttpRequest构造函数,所以能到成功导出xhr适配器。

  • httpAdapter
// lib/adapters/http.jsconst isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
export default isHttpAdapterSupported && function httpAdapter(config) {}

在浏览器环境不存在node的process对象,所以不能成功导出http适配器,为null

回到dispatchRequest方法:获取到adapter适配器后,即立刻使用适配器【即xhr】发起http请求,所以我们要了解具体的发起过程,就要理解xhr适配器的原理:

xhr

查看xhr适配器源码:

# xhr适配器
export default function (config) {
  // 返回一个Promise对象,传入一个executor执行器函数
  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 (utils.isFormData(requestData) && (platform.isStandardBrowserEnv || platform.isStandardBrowserWebWorkerEnv)) {
      requestHeaders.setContentType(false); // Let the browser set it
    }
​
    # 创建xhr请求对象
    let request = new XMLHttpRequest();
​
    // HTTP basic authentication
    # 权限配置
    if (config.auth) {
      const username = config.auth.username || '';
      const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password));
    }
​
    const fullPath = buildFullPath(config.baseURL, config.url);
​
    // 请求配置
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
​
    // Set the request timeout in MS
    # 本次http请求的超时设置
    request.timeout = config.timeout;
​
    function onloadend() {}
​
    # 配置http请求完成的钩子函数
    if ('onloadend' in request) {
      request.onloadend = onloadend;
    } else {
      request.onreadystatechange = function handleLoad() {};
    }
​
    # 请求取消钩子函数
    request.onabort = function handleAbort() {};
​
    # 请求错误处理
    request.onerror = function handleError() {};
​
    # 请求超时处理
    request.ontimeout = function handleTimeout() {};
​
    // Add xsrf header
    // This is only done if running in a standard browser environment.
    // Specifically not if we're in a web worker, or react-native.
    if (platform.isStandardBrowserEnv) {
      // Add xsrf header
      const xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath))
        && config.xsrfCookieName && cookies.read(config.xsrfCookieName);
​
      if (xsrfValue) {
        requestHeaders.set(config.xsrfHeaderName, xsrfValue);
      }
    }
​
    // Remove Content-Type if data is undefined
    requestData === undefined && requestHeaders.setContentType(null);
​
    # 设置http请求头
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
        request.setRequestHeader(key, val);
      });
    }
​
    # 设置withCredentials凭证
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }
​
    # 设置响应类型
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }
​
    // Handle progress if needed
    # 设置下载进度监听
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
    }
​
    // Not all browsers support upload events
    # 设置上传进度监听
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
    }
​
    # 如果配置取消cancel,则取消本次请求
    if (config.cancelToken || config.signal) {
        // 取消请求:其实就是在abort内部调用reject方法
        request.abort();
    }
​
    const protocol = parseProtocol(fullPath);
​
    if (protocol && platform.protocols.indexOf(protocol) === -1) {
      reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
      return;
    }
​
    # 发起http请求
    request.send(requestData || null);
  });
}

xhr适配器的源码虽然看着有点多,但是并不复杂,我们可以分为以下几个部分:

  • 使用原生XMLHttpRequest构造函数创建request请求对象。
  • 使用request.open配置本次http请求方式,请求地址【默认为异步请求】。
  • 使用request.setRequestHeader设置本次http的请求头。
  • 注册request请求成功,请求失败,请求超时,请求取消,请求进度等钩子函数。
  • 在发起本次请求之前,如果配置了cancelToken/signal,可以取消请求。
  • 使用request.send发起http请求。

到这里我们也可以进行一个总结:axios插件底层原理就是基于原生的XMLHttpRequest构造函数。

// 创建请求对象
const xhr = new XMLHttpRequest()
// 请求配置【默认为异步请求】
xhr.open(method, url, true)
// 发送请求
xhr.send()

扩展: XMLHttpRequest 有两种执行模式:同步(synchronous)和异步(asynchronous)。如果在 open 方法中将第三个参数 async 设置为 false,那么请求就会以同步的方式进行。换句话说,JavaScript 执行在 send() 处暂停,并在收到响应后恢复执行。这有点儿像 alertprompt 命令,这会阻塞程序的运行。所以在默认情况下,xhr都会为异步请求。

我们再回到dispatchRequest方法的最后:

export default function dispatchRequest(config) {
  return adapter(config).then(function onAdapterResolution(response) {
    # 请求成功的情况下
    // 处理响应数据
    response.data = transformData.call(
      config,
      config.transformResponse,
      response
    );
    
    // 处理响应头
    response.headers = AxiosHeaders.from(response.headers);
    // 返回响应式内容
    return response;
      
  }, function onAdapterRejection(reason) {
​
    # 请求失败的情况下:返回错误信息
    return Promise.reject(reason);
  });
}

这里的adapter就是我们的xhr适配器,当我们调用adapter(config)时,就会使用xhr适配器发起http请求:

  • 请求成功:则会执行onAdapterResolution,返回最终的响应内容。
  • 请求失败:则会执行onAdapterRejection,返回错误信息。

以上就是使用axios来发起一个http请求的完整流程。

下面我们可以根据几个常见的情况来进行验证:

(一)请求超时

request.ontimeout = function handleTimeout() {
    # 调用rejtct方法,返回错误信息对象
    reject(new AxiosError(
        timeoutErrorMessage,
        transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
        config,
        request));
​
    // 清除request对象
    request = null;
}

请求超时:会调用rejtct方法,返回错误信息对象,即会执行onAdapterRejection方法,返回最终的错误信息。

(二)请求错误

// 请求错误处理
request.onerror = function handleError() {
   reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
​
   // Clean up request
   request = null;
};

请求错误:同样会调用rejtct方法,返回错误信息对象,即会执行onAdapterRejection方法,返回最终的错误信息。

(三)取消请求

request.onabort = function handleAbort() {
   if (!request) {
      return;
   }
​
   reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
​
   // Clean up request
   request = null;
};

取消请求:调用rejtct方法,返回错误信息对象,即会执行onAdapterRejection方法,返回最终的错误信息。

(四)请求成功

# 请求完成
function onloadend() {
  if (!request) {
    return;
  }
  // Prepare the response
  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
  };
​
  settle(function _resolve(value) {
    # 请求成功,调用resolve,返回响应数据
    resolve(value);
      
    // 取消一些订阅
    done();
  }, function _reject(err) {
    // 出现异常,调用reject
    reject(err);
    done();
  }, response);
​
  // Clean up request
  request = null;
}

请求成功:调用resolve方法,返回响应数据,即会执行onAdapterResolution方法,返回最终的响应内容。

最后再看一下http请求进度的监听:

  • 下载进度:
// 下载进度监听
if (typeof config.onDownloadProgress === 'function') {
   request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
}

我们只需要在本次请求的config中配置onDownloadProgress函数即可:

export function myDownload(data, onDownloadProgress) {
    return axios({
        url: '/api/download',
        method: 'post',
        data,
        onDownloadProgress,
    })
}
​
// 调用
myDownload(data, (e) => {})
  • 上传进度:
// 上传进度监听
if (typeof config.onUploadProgress === 'function' && request.upload) {
   request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
}

我们只需要在本次请求的config中配置onUploadProgress函数即可:

export function myUpload(data, onUploadProgress) {
    return axios({
        url: '/api/download',
        method: 'post',
        data,
        onUploadProgress,
    })
}
​
// 调用
myUpload(data, (e) => {})

5,完整请求流程

axios源码有了基本的了解后,我们使用一个简单的案例来过一遍完整的请求流程。

import axios from "axios";
// 创建请求实例
const service = axios.create({
        baseURL: import.meta.env.VITE_APP_BASE_API,
        timeout: 10000
});
// 请求拦截器
service.interceptors.request.use(
        (config) => {
            // 每个请求接口的公共参数
            config.headers.Authorization = "myToken";
            return config;
        },
        (error) => {
            return Promise.reject(error);
        }
    );
// 响应拦截器
service.interceptors.response.use(
        (res) => {
            console.log(res)
            return res;
        },
        (error) => {
            // 可以提示请求出错
            console.log('请求失败', error)
            return Promise.reject(error);
        }
    );
// 发起请求
service({
        method: 'get',
        url: 'https://www.fastmock.site/mock/324a7f3da68215e1834d217d35d1c7/api/userInfo',
        timeout: 5000,
        onDownloadProgress(e) {
            console.log(e)
        }
    })
创建请求实例

进入断点,首先调用axios.create创建请求实例service

1.png

返回的是createInstance方法,首先调用了mergeConfig函数来合并生成全局config

2.png

3.png

4.png

5.png

6.png

7.png

81.png

这里的context就是创建完成的axios实例,在创建完成后,立即执行一个bind方法,将请求实例包装成了一个函数实例。

1.png

2.png

3.png

4.png

5.png

6.png

设置拦截器

下面开始设置axios拦截器:

7.png

8.png

这里我们没有传递第三个参数options,所以后面两个为falsenull

9.png

响应拦截器的添加同理,就不重复贴图了。

11.png

发起请求

0.png

进入request方法,开始发起请求:

1.png

2.png

3.png

4.png

5.png

6.png

7.png

8.png

9.png

11.png

12.png

13.png

14.png

当然:实际情况是将执行链钩子添加到了微任务队列,等待异步执行,我们这里为了方便阅读直接进入执行阶段。

触发请求拦截器

1.png

然后执行dispatchRequest,发起http请求:

2.png

3.png

4.png

5.png

6.png

7.png

8.png

9.png

99.png

触发http请求

1.png

2.png

3.png

4.png

5.png

6.png

触发响应拦截器

7.png

请求完成

8.png