27

前言

axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。这里将会从功能出发,分析源码,深入了解 axios 是怎么实现这些功能的。

准备

IDE: WebStorm
Git地址: https://github.com/cookhot/ax... 注意analysis分支
中文文档: https://www.kancloud.cn/yunye...

axios 请求

项目的入口是axios.js, 当axios在被引入项目中的时候,导入的其实是一个方法,可以直接调用此方法发起请求。

例子如下:

import axios from './axios'
console.log(typeof axios); // function
axios({
  url: 'http://localhost:8088/index'
}).then((res) => {
  console.log(res)
})

源码如下:

axios.js

'use strict';

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var defaults = require('./defaults');

/**
 * 创建 Axios 的一个实例
 * Create an instance of Axios
 * 
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);

  // instance 是一个方法, 实际上就是 Axios.prorotype.request, 方法的 this => context
  var instance = bind(Axios.prototype.request, context);

  // 把 Axios 原型上面的属性(方法)复制到 instance 上面,保证被复制的方法中 this => context
  // 注意 utils.extend 和 utils.merge的区别,两者是不同的
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  // context 上面的属性都复制到 instance,context.defaults 和 context.interceptors 通过instance能够访问
  utils.extend(instance, context);

  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Factory for creating new instances
// 创建 Axios 实例
axios.create = function create(instanceConfig) {
  // instanceConfig 是开发者提供的配置属性,将会和 Axios 提供的默认配置属性合并,
  // 形成的新的配置属性将会是实例请求的默认属性 (很常用的设计方法)
  return createInstance(utils.merge(defaults, instanceConfig));
};

// Expose Cancel & CancelToken
// 请求取消
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');

// 输出Axios
module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;

从上面的源码中,能够看到axios其实就是调用的Axios.prototype.request方法,为了防止在运行时候this指向异常,显示的绑定上了context。

// ...... bind 的实现
module.exports = function bind(fn, thisArg) {
   // 使用闭包 和 apply 改变 fn 的 this 指向
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};

为了能够让开发人员更好的调用get、post、...... 等等方法, 于是把Axios.prototype上面的方法都复制到axios上面。 相应的为了防止这些方法中this指向异常,也显示的绑定context, 具体的实现逻辑请看下面 ⤵️ 对象的复制。 后面的 utils.extend(instance, context) 这行代码是为了帮助我们能够通过axios 访问到 context 上面的属性, context里面包含拦截器(interceptors)以及配置属性值(defaults)。

对象的复制

axios提供了两种的方式来处理对象的合并, 分别是 merge 与 extend。代码被放在utils.js

// utils.js
// .......
/**
 * Iterate over an Array or an Object invoking a function for each item.
 *
 * If `obj` is an Array callback will be called passing
 * the value, index, and complete array for each item.
 *
 * If 'obj' is an Object callback will be called passing
 * the value, key, and complete object for each property.
 *
 * @param {Object|Array} obj The object to iterate
 * @param {Function} fn The callback to invoke for each item
 */
function forEach(obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // Force an array if not already something iterable
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // Iterate over object keys
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

/**
 * Accepts varargs expecting each argument to be an object, then
 * immutably merges the properties of each object and returns result.
 *
 * When multiple objects contain the same key the later object in
 * the arguments list will take precedence.
 *
 * Example:
 *
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // outputs 456
 * ```
 *
 * @param {Object} obj1 Object to merge
 * @returns {Object} Result of all merge properties
 */
function merge(/* obj1, obj2, obj3, ... */) {
  // 使用新的对象,这样就能防止传入的对象在合并的时候被改变
  var result = {};
  function assignValue(val, key) {
   // 对象的属性复制的时候,当两个属性都是都是对象的时候,就对此属性对象中子属性再进行在复制。
    // 作用应该是为了防止前属性对象的属性全被覆盖掉
    if (typeof result[key] === 'object' && typeof val === 'object') {
      result[key] = merge(result[key], val);
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

/**
 * Extends object a by mutably adding to it the properties of object b.
 *
 * 这里考虑了复制函数时候的 this 指向问题,设计的很好,以后可以借鉴
 * @param {Object} a The object to be extended    
 * @param {Object} b The object to copy properties from
 * @param {Object} thisArg The object to bind function to
 * @return {Object} The resulting value of object a
 */
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

merge 类似于我们经常使用的对象浅拷贝,但是又不全是浅拷贝。在拷贝的时候,发现进行拷贝的两个属性都是都是对象的时候,就对此属性对象中子属性再进行在复制。用于防止前面一个属性对象中的子属性值被全覆盖掉。
extend 也是对象的浅拷贝,不过在拷贝方法的时候,会显示指定方法的this,用于防止this指向异常。

all以及cancel

axios创建请求后会返回一个Promise的实例,Promise.all所返回的promise实例会在传入的promise实例状态都发生变化,才变更状态。所以 axios.all其实就是调用Promise.all

axios.all = function all(promises) {
  return Promise.all(promises);
};

cancel 这里暂时不讨论,后面通过结合 XMLHttpRequest 与 node 的 http 会说的明白的更加清楚。

请求复用与拦截器

在看完axios.js以后,就需要开始了解Axios构造函数的实现了。

源码如下:

Axios.js

'use strict';

var defaults = require('./../defaults');
var utils = require('./../utils');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 */
function Axios(instanceConfig) {
  // instanceConfig => 创建对象的设置的默认值
  // Axios 中 defaults 分为三个层次, Axios 默认的defaults < 创建实例传入的defaults < 调用方法时候传入的defaults
  // 个人感觉使用 this.defaults = utils.merge(defaults, instanceConfig) 会更好,当后面使用request发起请求的时候,代码变化如下:
 /*
    config = utils.merge(defaults, this.defaults, config); 老代码
    config = utils.merge(this.defaults, config); // 新代码
  */
  this.defaults = instanceConfig;
  // 拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  // 重载 request(url, config)
  // 可以支持request (config) 也可以支持 request(url, config)
  if (typeof config === 'string') {
    config = utils.merge({
      url: arguments[0]
    }, arguments[1]);
  }

  config = utils.merge(defaults, this.defaults, config);
  config.method = config.method.toLowerCase();

  // Hook up interceptors middleware
  // 拦截器设计处理
  // chain 是一个数组
  var chain = [dispatchRequest, undefined];
  // promise 是调用头,状态已经改变为 resolved
  var promise = Promise.resolve(config);

  // 使用 use 添加 fulfilled 与 rejected 添加到队列中
  // 添加 request 拦截函数的时候使用的是unshift, 这样会导致 use 后添加的先执行,先添加的后执行
  /*
  axios.interceptors.request.use(function resolve(config) {
    console.log("1");
  });

  axios.interceptors.request.use(function resolve(config) {
    console.log("2")
  })
  // 结果 2 1
   */

  // 考虑到后面 是使用 promise的链式调用, 所以在 拦截器的回调方法中 必须要返回一个 config 对象
  // 如果不返回 config, 会导致后续请求执行异常

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // response 使用的push 添加 拦截函数,这里是添加先执行,后添加后执行
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // promise 的初始化状态就是 resolved,这里形成了promise调用链,执行流程过程如下

  // chain  [fulfilled, rejected, ... dispatchRequest, undefined ....,fulfilled, rejected]
  // 这里补充一下 fulfilled, rejected 都是肯定是成对出现的, 具体原因可看 InterceptorManager.prototype.use
  // promise.then(undefined, undefined) 中当传递的不是function时,会发生值穿。也就是说 use 中可以传入非function,
  // 或者传入单个function
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

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

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

module.exports = Axios;

Axios.js主要处理了两个部分,复用请求方法、实现拦截器。

当我们使用 Axios 的实例去发送请求,使用的方法get、post等都是复用了request方法,在request方法中通过 arguments 获取传入的参数,实现了传入参数的重载。

拦截器是axios的一大特色,它的实现原理其实不复杂,核心就是promise的链式调用。
原理可以参考下图:
clipboard.png
然后附上InterceptorManager.js的源码,但是个人觉得这里没有什么好说的,其实就是对一个数组的操作。

'use strict';

var utils = require('./../utils');

function InterceptorManager() {
  this.handlers = [];
}

/**
 * Add a new interceptor to the stack
 *
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} An ID used to remove interceptor later
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  // fulfilled => 成功方法
  // rejected => 失败方法
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

/**
 * Remove an interceptor from the stack
 *
 * @param {Number} id The ID that was returned by `use`
 */
// 把数组中 对象设置为 null
InterceptorManager.prototype.reject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

/**
 * Iterate over all the registered interceptors
 *
 * This method is particularly useful for skipping over any
 * interceptors that may have become `null` calling `eject`.
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  // 遍历运行数组
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

如果你看完上面的源码解读,能够清楚的明白下面这段代码执行顺序,那么就说明你掌握axios拦截器的实现了

  // 拦截器的执行顺序
  /*
  axios.interceptors.request.use(function resolve(config) {
    console.log("request");
    return config;
  });

  axios.interceptors.response.use(function resolve(res) {
    console.log('response')
    return res
  });

  axios.get('http://localhost:3000/index').then(function resolve(res) {
      console.log('ajax');
      return res
  }).then(function(){
    console.log('end')
  })
   */

第一篇总算是写完了,后续还有关于axios3篇的源码解读,如果大家觉得写的还行的话,麻烦给一个赞?,鼓励鼓励,谢谢了


火星田园犬
933 声望685 粉丝

小心驾驶, 专业埋雷