axios源碼解讀之請求與攔截器

前言

axios 是一個基於 promise 的 HTTP 庫,能夠用在瀏覽器和 node.js 中。這裏將會從功能出發,分析源碼,深刻了解 axios 是怎麼實現這些功能的。node

準備

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

axios 請求

項目的入口是axios.js, 當axios在被引入項目中的時候,導入的實際上是一個方法,能夠直接調用此方法發起請求。git

例子以下:

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

源碼以下:

axios.jsgithub

'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。ajax

// ...... 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

對象的複製

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指向異常。promise

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篇的源碼解讀,若是你們以爲寫的還行的話,麻煩給一個贊?,鼓勵鼓勵,謝謝了

相關文章
相關標籤/搜索