面试官:你理解axios的原理吗?有看过它的源码吗?

一、axios的根本应用

对于 axios 的根本应用,上篇文章曾经有所波及,这里再略微回顾一下:

发送申请

import axios from 'axios';axios(config) // 间接传入配置axios(url[, config]) // 传入url和配置axios[method](url[, option]) // 间接调用申请形式办法,传入url和配置axios[method](url[, data[, option]]) // 间接调用申请形式办法,传入data、url和配置axios.request(option) // 调用 request 办法const axiosInstance = axios.create(config)// axiosInstance 也具备以上 axios 的能力axios.all([axiosInstance1, axiosInstance2]).then(axios.spread(response1, response2))// 调用 all 和传入 spread 回调

申请拦截器

axios.interceptors.request.use(function (config) {    // 这里写发送申请前解决的代码    return config;}, function (error) {    // 这里写发送申请谬误相干的代码    return Promise.reject(error);});

响应拦截器

axios.interceptors.response.use(function (response) {    // 这里写失去响应数据后处理的代码    return response;}, function (error) {    // 这里写失去谬误响应解决的代码    return Promise.reject(error);});

勾销申请

// 形式一const CancelToken = axios.CancelToken;const source = CancelToken.source();axios.get('xxxx', {  cancelToken: source.token})// 勾销申请 (申请起因是可选的)source.cancel('被动勾销申请');// 形式二const CancelToken = axios.CancelToken;let cancel;axios.get('xxxx', {  cancelToken: new CancelToken(function executor(c) {    cancel = c;  })});cancel('被动勾销申请');

二、实现一个简易版axios

构建一个 Axios 构造函数,外围代码为 request 办法:

class Axios {    constructor() {    }    request(config) {        return new Promise(resolve => {            const {url = '', method = 'get', data = {}} = config;            // 发送ajax申请            const xhr = new XMLHttpRequest();            xhr.open(method, url, true);            xhr.onload = function() {                console.log(xhr.responseText)                resolve(xhr.responseText);            }            xhr.send(data);        })    }}

导出 axios 实例:

// 最终导出axios的办法,即实例的request办法function CreateAxiosFn() {    let axios = new Axios();    let req = axios.request.bind(axios);    return req;}// 失去最初的全局变量axioslet axios = CreateAxiosFn();

上述就曾经可能实现 axios({ }) 这种形式的申请。上面是来实现下 axios.method() 这种模式的申请:

// 定义get,post...办法,挂在到Axios原型上const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];methodsArr.forEach(met => {    Axios.prototype[met] = function() {        console.log('执行'+met+'办法');        // 解决单个办法        if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config])            return this.request({                method: met,                url: arguments[0],                ...arguments[1] || {}            })        } else { // 3个参数(url[,data[,config]])            return this.request({                method: met,                url: arguments[0],                data: arguments[1] || {},                ...arguments[2] || {}            })        }    }})

Axios.prototype 上的办法搬运到 request 上。首先实现一个工具类,实现将 b 办法混入到 a,并且批改 this 指向:

const utils = {  extend(a,b, context) {    for(let key in b) {      if (b.hasOwnProperty(key)) {        if (typeof b[key] === 'function') {          a[key] = b[key].bind(context);        } else {          a[key] = b[key]        }      }          }  }}

批改导出的办法:

function CreateAxiosFn() {  let axios = new Axios();    let req = axios.request.bind(axios);  // 减少代码  utils.extend(req, Axios.prototype, axios)    return req;}

构建拦截器的构造函数:

class InterceptorsManage {  constructor() {    this.handlers = [];  }  use(fullfield, rejected) {    this.handlers.push({      fullfield,      rejected    })  }}

实现 axios.interceptors.response.useaxios.interceptors.request.use

class Axios {    constructor() {        // 新增代码        this.interceptors = {            request: new InterceptorsManage,            response: new InterceptorsManage        }    }    request(config) {        ...    }}

执行语句 axios.interceptors.response.useaxios.interceptors.request.use 的时候,实现获取 axios 实例上的 interceptors 对象,而后再获取 responserequest 拦截器,再执行对应的拦截器的 use 办法。

Axios 上的办法和属性搬到 request 过来:

function CreateAxiosFn() {  let axios = new Axios();    let req = axios.request.bind(axios);  // 混入办法, 解决axios的request办法,使之领有get,post...办法  utils.extend(req, Axios.prototype, axios)  // 新增代码  utils.extend(req, axios)  return req;}

当初 request 也有了 interceptors 对象,在发送申请的时候,会先获取 request 拦截器的 handlers 的办法来执行。

首先将执行 ajax 的申请封装成一个办法:

request(config) {    this.sendAjax(config)}sendAjax(config){    return new Promise(resolve => {        const {url = '', method = 'get', data = {}} = config;        // 发送ajax申请        console.log(config);        const xhr = new XMLHttpRequest();        xhr.open(method, url, true);        xhr.onload = function() {            console.log(xhr.responseText)            resolve(xhr.responseText);        };        xhr.send(data);    })}

取得 handlers 中的回调:

request(config) {    // 拦截器和申请组装队列    let chain = [this.sendAjax.bind(this), undefined] // 成对呈现的,失败回调临时不解决    // 申请拦挡    this.interceptors.request.handlers.forEach(interceptor => {        chain.unshift(interceptor.fullfield, interceptor.rejected)    })    // 响应拦挡    this.interceptors.response.handlers.forEach(interceptor => {        chain.push(interceptor.fullfield, interceptor.rejected)    })    // 执行队列,每次执行一对,并给promise赋最新的值    let promise = Promise.resolve(config);    while(chain.length > 0) {        promise = promise.then(chain.shift(), chain.shift())    }    return promise;}

chains 大略是 ['fulfilled1','reject1','fulfilled2','reject2','this.sendAjax','undefined','fulfilled2','reject2','fulfilled1','reject1'] 这种模式。这样就可能胜利实现一个简易版 axios

三、源码剖析

首先看看目录构造:

axios 发送申请有很多实现的办法,实现入口文件为 axios.js

function createInstance(defaultConfig) {  var context = new Axios(defaultConfig);  // instance指向了request办法,且上下文指向context,所以能够间接以 instance(option) 形式调用   // Axios.prototype.request 内对第一个参数的数据类型判断,使咱们可能以 instance(url, option) 形式调用  var instance = bind(Axios.prototype.request, context);  // 把Axios.prototype上的办法扩大到instance对象上,  // 并指定上下文为context,这样执行Axios原型链上的办法时,this会指向context  utils.extend(instance, Axios.prototype, context);  // Copy context to instance  // 把context对象上的本身属性和办法扩大到instance上  // 注:因为extend外部应用的forEach办法对对象做for in 遍历时,只遍历对象自身的属性,而不会遍历原型链上的属性  // 这样,instance 就有了  defaults、interceptors 属性。  utils.extend(instance, context);  return instance;}// Create the default instance to be exported 创立一个由默认配置生成的axios实例var axios = createInstance(defaults);// Factory for creating new instances 扩大axios.create工厂函数,外部也是 createInstanceaxios.create = function create(instanceConfig) {  return createInstance(mergeConfig(axios.defaults, instanceConfig));};// Expose all/spreadaxios.all = function all(promises) {  return Promise.all(promises);};axios.spread = function spread(callback) {  return function wrap(arr) {    return callback.apply(null, arr);  };};module.exports = axios;

次要外围是 Axios.prototype.request,各种申请形式的调用实现都是在 request 外部实现的,简略看下 request 的逻辑:

Axios.prototype.request = function request(config) {  // Allow for axios('example/url'[, config]) a la fetch API  // 判断 config 参数是否是 字符串,如果是则认为第一个参数是 URL,第二个参数是真正的config  if (typeof config === 'string') {    config = arguments[1] || {};    // 把 url 搁置到 config 对象中,便于之后的 mergeConfig    config.url = arguments[0];  } else {    // 如果 config 参数是否是 字符串,则整体都当做config    config = config || {};  }  // 合并默认配置和传入的配置  config = mergeConfig(this.defaults, config);  // 设置申请办法  config.method = config.method ? config.method.toLowerCase() : 'get';  /*    something... 此局部会在后续拦截器独自讲述  */};// 在 Axios 原型上挂载 'delete', 'get', 'head', 'options' 且不传参的申请办法,实现外部也是 requestutils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {  Axios.prototype[method] = function(url, config) {    return this.request(utils.merge(config || {}, {      method: method,      url: url    }));  };});// 在 Axios 原型上挂载 'post', 'put', 'patch' 且传参的申请办法,实现外部同样也是 requestutils.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 入口参数为 config,能够说 config 贯彻了 axios 的毕生。axios 中的 config 次要散布在这几个中央:

  • 默认配置 defaults.js
  • config.method 默认为 get
  • 调用 createInstance 办法创立 axios 实例,传入的 config
  • 间接或间接调用 request 办法,传入的 config
// axios.js// 创立一个由默认配置生成的axios实例var axios = createInstance(defaults);// 扩大axios.create工厂函数,外部也是 createInstanceaxios.create = function create(instanceConfig) {  return createInstance(mergeConfig(axios.defaults, instanceConfig));};// Axios.js// 合并默认配置和传入的配置config = mergeConfig(this.defaults, config);// 设置申请办法config.method = config.method ? config.method.toLowerCase() : 'get';

从源码中,能够看到优先级:默认配置对象 default < method:get < Axios的实例属性 this.default < request参数。

上面重点看看 request 办法:

Axios.prototype.request = function request(config) {  /*    先是 mergeConfig ... 等,不再论述  */  // Hook up interceptors middleware 创立拦截器链。dispatchRequest 是重中之重,后续重点  var chain = [dispatchRequest, undefined];  // push各个拦截器办法 留神:interceptor.fulfilled 或 interceptor.rejected 是可能为undefined  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {    // 申请拦截器逆序 留神此处的 forEach 是自定义的拦截器的forEach办法    chain.unshift(interceptor.fulfilled, interceptor.rejected);  });  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {    // 响应拦截器程序 留神此处的 forEach 是自定义的拦截器的forEach办法    chain.push(interceptor.fulfilled, interceptor.rejected);  });  // 初始化一个 promise 对象,状态为 resolved,接管到的参数为曾经解决合并过的 config 对象  var promise = Promise.resolve(config);  // 循环拦截器的链  while (chain.length) {    promise = promise.then(chain.shift(), chain.shift()); // 每一次向外弹出拦截器  }  // 返回 promise  return promise;};

拦截器 interceptors 是在构建 axios 实例化的属性:

function Axios(instanceConfig) {  this.defaults = instanceConfig;  this.interceptors = {    request: new InterceptorManager(), // 申请拦挡    response: new InterceptorManager() // 响应拦挡  };}

InterceptorManager 构造函数:

// 拦截器的初始化 其实就是一组钩子函数function InterceptorManager() {  this.handlers = [];}// 调用拦截器实例的 use 时就是往钩子函数中 push 办法InterceptorManager.prototype.use = function use(fulfilled, rejected) {  this.handlers.push({    fulfilled: fulfilled,    rejected: rejected  });  return this.handlers.length - 1;};// 拦截器是能够勾销的,依据 use 的时候返回的 ID,把某一个拦截器办法置为 null// 不能用 splice 或者 slice 的起因是 删除之后 id 就会变动,导致之后的程序或者是操作不可控InterceptorManager.prototype.eject = function eject(id) {  if (this.handlers[id]) {    this.handlers[id] = null;  }};// 这就是在 Axios 的 request 办法中 中循环拦截器的办法 forEach 循环执行钩子函数InterceptorManager.prototype.forEach = function forEach(fn) {  utils.forEach(this.handlers, function forEachHandler(h) {    if (h !== null) {      fn(h);    }  });}

申请拦截器办法是被 unshift 到拦截器中,响应拦截器是被 push 到拦截器中的。最终它们会拼接上一个叫 dispatchRequest 的办法被后续的 promise 程序执行:

var utils = require('./../utils');var transformData = require('./transformData');var isCancel = require('../cancel/isCancel');var defaults = require('../defaults');var isAbsoluteURL = require('./../helpers/isAbsoluteURL');var combineURLs = require('./../helpers/combineURLs');// 判断申请是否已被勾销,如果曾经被勾销,抛出已勾销function throwIfCancellationRequested(config) {  if (config.cancelToken) {    config.cancelToken.throwIfRequested();  }}module.exports = function dispatchRequest(config) {  throwIfCancellationRequested(config);  // 如果蕴含 baseUrl,并且不是 config.url 绝对路径,组合 baseUrl 以及 config.url  if (config.baseURL && !isAbsoluteURL(config.url)) {    // 组合 baseURL 与 url 造成残缺的申请门路    config.url = combineURLs(config.baseURL, config.url);  }  config.headers = config.headers || {};  // 应用 /lib/defaults.js 中的 transformRequest 办法,对 config.headers 和 config.data 进行格式化  // 比方将 headers 中的 Accept,Content-Type 对立解决成大写  // 比方如果申请注释是一个 Object 会格式化为 JSON 字符串,并增加 application/json;charset=utf-8 的 Content-Type  // 等一系列操作  config.data = transformData(    config.data,    config.headers,    config.transformRequest  );  // 合并不同配置的 headers,config.headers 的配置优先级更高  config.headers = utils.merge(    config.headers.common || {},    config.headers[config.method] || {},    config.headers || {}  );  // 删除 headers 中的 method 属性  utils.forEach(    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],    function cleanHeaderConfig(method) {      delete config.headers[method];    }  );  // 如果 config 配置了 adapter,应用 config 中配置 adapter 的代替默认的申请办法  var adapter = config.adapter || defaults.adapter;  // 应用 adapter 办法发动申请(adapter 依据浏览器环境或者 Node 环境会有不同)  return adapter(config).then(    // 申请正确返回的回调    function onAdapterResolution(response) {      // 判断是否曾经勾销了申请,如果勾销了申请抛出已勾销      throwIfCancellationRequested(config);      // 应用 /lib/defaults.js 中的 transformResponse 办法,对服务器返回的数据进行格式化      // 例如,应用 JSON.parse 对响应注释进行解析      response.data = transformData(        response.data,        response.headers,        config.transformResponse      );      return response;    },    // 申请失败的回调    function onAdapterRejection(reason) {      if (!isCancel(reason)) {        throwIfCancellationRequested(config);        if (reason && reason.response) {          reason.response.data = transformData(            reason.response.data,            reason.response.headers,            config.transformResponse          );        }      }      return Promise.reject(reason);    }  );};
  • dispatchRequest 中首先判断是否曾经勾销了申请,如果已勾销,抛出已勾销
  • 合并不同配置的 headers,config.headers 的配置优先级更高
  • 应用 config.adapter 发动申请(adapter 依据浏览器环境或者 Node 环境会有不同)

源码中有一个比拟乏味的点是 transformData 这个办法:

var utils = require('./../utils');var defaults = require('../defaults');module.exports = function transformData(data, headers, fns) {  /*eslint no-param-reassign:0*/  utils.forEach(fns, function transform(fn) {    data = fn(data, headers);  });  return data;};

transformData 其实就是执行 config.transformRequestconfig.transformResponse

// 应用 /lib/defaults.js 中的 transformRequest 办法,对 config.headers 和 config.data 进行格式化// 比方将 headers 中的 Accept,Content-Type 对立解决成大写// 比方如果申请注释是一个 Object 会格式化为 JSON 字符串,并增加 application/json;charset=utf-8 的 Content-Type// 等一系列操作config.data = transformData(  config.data,  config.headers,  config.transformRequest);// 应用 /lib/defaults.js 中的 transformResponse 办法,对服务器返回的数据进行格式化// 例如,应用 JSON.parse 对响应注释进行解析response.data = transformData(  response.data,  response.headers,  config.transformResponse);

四、总结

axios 是一个基于 Promise 的 HTTP 申请客户端,具备弱小的拦截器性能,反对申请和响应的拦挡解决,同时提供了丰盛的配置选项。在理论开发中,能够利用 axios 来发动 HTTP 申请,解决响应数据,实现前后端的数据交互。通过深刻了解 axios 的原理和源码,能够更好地应用和扩大它,满足简单的业务需要。

参考文献

  • https://juejin.cn/post/6856706569263677447#heading-4
  • https://juejin.cn/post/6844903907500490766
  • https://github.com/axios/axios