學習 axios 源碼總體架構,打造屬於本身的請求庫

前言

這是 學習源碼總體架構系列第六篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。

學習源碼總體架構系列文章以下:javascript

1.學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
2.學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
3.學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫
4.學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK
5.學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫 html

感興趣的讀者能夠點擊閱讀。下一篇多是vue-router源碼。前端

本文比較長,手機上閱讀,能夠劃到有圖的地方直接看文中的幾張圖便可。建議點贊或收藏後在電腦上閱讀,按照文中調試方式本身調試或許更容易吸取消化。vue

導讀
文章詳細介紹了axios調試方法。詳細介紹了axios構造函數,攔截器,取消等功能的實現。最後還對比了其餘請求庫。java

本文學習的版本是v0.19.0。克隆的官方倉庫的master分支。 截至目前(2019年12月14日),最新一次commit2019-12-09 15:52 ZhaoXC`dc4bc49673943e352fix: fix ignore set withCredentials false (#2582)`。node

本文倉庫在這裏若川的 axios-analysis github 倉庫。求個star呀。webpack

若是你是求職者,項目寫了運用了axios,面試官可能會問你:ios

1.爲何 axios既能夠當函數調用,也能夠當對象使用,好比 axios({})axios.get
2.簡述 axios調用流程。
3.有用過攔截器嗎?原理是怎樣的?
4.有使用 axios的取消功能嗎?是怎麼實現的?
5.爲何支持瀏覽器中發送請求也支持 node發送請求?
諸如這類問題。

chrome 和 vscode 調試 axios 源碼方法

前不久,筆者在知乎回答了一個問題一年內的前端看不懂前端框架源碼怎麼辦?推薦了一些資料,閱讀量還不錯,你們有興趣能夠看看。主要有四點:git

1.藉助調試
2.搜索查閱相關高贊文章
3.把不懂的地方記錄下來,查閱相關文檔
4.總結

看源碼,調試很重要,因此筆者詳細寫下axios源碼調試方法,幫助一些可能不知道如何調試的讀者。es6

chrome 調試瀏覽器環境的 axios

調試方法

axios打包後有sourcemap文件。

# 能夠克隆筆者的這個倉庫代碼
git clone https://github.com/lxchuan12/axios-analysis.git
cd axios-analaysis/axios
npm install
npm start
# open [http://localhost:3000](http://localhost:3000)
# chrome F12 source 控制面板  webpack//   .  lib 目錄下,根據狀況自行斷點調試

本文就是經過上述的例子axios/sandbox/client.html來調試的。

順便簡單提下調試example的例子,雖然文章最開始時寫了這部分,後來又刪了,最後想一想仍是寫下。

找到文件axios/examples/server.js,修改代碼以下:

server = http.createServer(function (req, res) {
  var url = req.url;
  // 調試 examples
  console.log(url);
  // Process axios itself
  if (/axios\.min\.js$/.test(url)) {
    // 原來的代碼 是 axios.min.js
    // pipeFileToResponse(res, '../dist/axios.min.js', 'text/javascript');
    pipeFileToResponse(res, '../dist/axios.js', 'text/javascript');
    return;
  }
  // 原來的代碼 是 axios.min.map
  // if (/axios\.min.map$/.test(url)) {
  if (/axios\.map$/.test(url)) {
    // 原來的代碼 是 axios.min.map
    // pipeFileToResponse(res, '../dist/axios.min.map', 'text/javascript');
    pipeFileToResponse(res, '../dist/axios.map', 'text/javascript');
    return;
  }
}
# 上述安裝好依賴後
# npm run examples 不能同時開啓,默認都是3000端口
# 能夠指定端口 5000
# npm run examples ===  node ./examples/server.js
node ./examples/server.js -p 5000

打開http://localhost:5000,而後就能夠開心的在Chrome瀏覽器中調試examples裏的例子了。

axios是支持node環境發送請求的。接下來看如何用vscode調試node環境下的axios

vscode 調試 node 環境的 axios

在根目錄下axios-analysis/建立.vscode/launch.json文件以下:

{
    // 使用 IntelliSense 瞭解相關屬性。
    // 懸停以查看現有屬性的描述。
    // 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/axios/sandbox/client.js",
            "skipFiles": [
                "<node_internals>/**"
            ]
        },
    ]
}

F5開始調試便可,按照本身的狀況,單步跳過(F10)、單步調試(F11)斷點調試。

其實開源項目通常都有貢獻指南axios/CONTRIBUTING.md,筆者只是把這個指南的基礎上修改成引用sourcemap的文件可調試。

先看 axios 結構是怎樣的

git clone https://github.com/lxchuan12/axios-analysis.git
cd axios-analaysis/axios
npm install
npm start

按照上文說的調試方法,npm start後,直接在chrome瀏覽器中調試。 打開http://localhost:3000,在控制檯打印出axios,估計不少人都沒打印出來看過。

console.log({axios: axios});

層層點開來看,axios的結構是怎樣的,先有一個大概印象。

筆者畫了一張比較詳細的圖表示。

axios 結構關係圖

看完結構圖,若是看過jQueryunderscorelodash源碼,會發現其實跟axios源碼設計相似。

jQuery 別名$underscore loadsh別名 _ 也既是函數,也是對象。好比jQuery使用方式。$('#id'),$.ajax

接下來看具體源碼的實現。能夠跟着斷點調試一下。

斷點調試要領:
賦值語句能夠一步跳過,看返回值便可,後續詳細再看。
函數執行須要斷點跟着看,也能夠結合註釋和上下文倒推這個函數作了什麼。

axios 源碼 初始化

看源碼第一步,先看package.json。通常都會申明main主入口文件。

// package.json
{
  "name": "axios",
  "version": "0.19.0",
  "description": "Promise based HTTP client for the browser and node.js",
  "main": "index.js",
  // ...
}

主入口文件

// index.js
module.exports = require('./lib/axios');

lib/axios.js主文件

axios.js文件 代碼相對比較多。分爲三部分展開敘述。

  1. 第一部分:引入一些工具函數utilsAxios構造函數、默認配置defaults等。
  2. 第二部分:是生成實例對象axiosaxios.Axiosaxios.create等。
  3. 第三部分取消相關API實現,還有allspread、導出等實現。

第一部分

引入一些工具函數utilsAxios構造函數、默認配置defaults等。

// 第一部分:
// lib/axios
// 嚴格模式
'use strict';
// 引入 utils 對象,有不少工具方法。
var utils = require('./utils');
// 引入 bind 方法
var bind = require('./helpers/bind');
// 核心構造函數 Axios
var Axios = require('./core/Axios');
// 合併配置方法
var mergeConfig = require('./core/mergeConfig');
// 引入默認配置
var defaults = require('./defaults');

第二部分

是生成實例對象axiosaxios.Axiosaxios.create等。

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  // new 一個 Axios 生成實例對象
  var context = new Axios(defaultConfig);
  // bind 返回一個新的 wrap 函數,
  // 也就是爲何調用 axios 是調用 Axios.prototype.request 函數的緣由
  var instance = bind(Axios.prototype.request, context);
  // Copy axios.prototype to instance
  // 複製 Axios.prototype 到實例上。
  // 也就是爲何 有 axios.get 等別名方法,
  // 且調用的是 Axios.prototype.get 等別名方法。
  utils.extend(instance, Axios.prototype, context);
  // Copy context to instance
  // 複製 context 到 intance 實例
  // 也就是爲何默認配置 axios.defaults 和攔截器  axios.interceptors 可使用的緣由
  // 實際上是new Axios().defaults 和 new Axios().interceptors
  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 class 容許 class 繼承 也就是能夠 new axios.Axios()
// 但  axios 文檔中 並無提到這個,咱們平時也用得少。
axios.Axios = Axios;

// Factory for creating new instances
// 工廠模式 建立新的實例 用戶能夠自定義一些參數
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

這裏簡述下工廠模式。axios.create,也就是用戶不須要知道內部是怎麼實現的。
舉個生活的例子,咱們買手機,不須要知道手機是怎麼作的,就是工廠模式。
看完第二部分,裏面涉及幾個工具函數,如bindextend。接下來說述這幾個工具方法。

工具方法之 bind

axios/lib/helpers/bind.js

'use strict';
// 返回一個新的函數 wrap
module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    // 把 argument 對象放在數組 args 裏
    return fn.apply(thisArg, args);
  };
};

傳遞兩個參數函數和thisArg指向。
把參數arguments生成數組,最後調用返回參數結構。
其實如今apply支持arguments這樣的類數組對象了,不須要手動轉數組。
那麼爲啥做者要轉數組,爲了性能?當時不支持?抑或是做者不知道?這就不得而知了。有讀者知道歡迎評論區告訴筆者呀。

關於applycallbind等不是很熟悉的讀者,能夠看筆者的另外一個面試官問系列
面試官問:可否模擬實現JS的bind方法

舉個例子

function fn(){
  console.log.apply(console, arguments);
}
fn(1,2,3,4,5,6, '若川');
// 1 2 3 4 5 6 '若川'

工具方法之 utils.extend

axios/lib/utils.js

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;
}

其實就是遍歷參數b對象,複製到a對象上,若是是函數就是則用bind調用。

工具方法之 utils.forEach

axios/lib/utils.js

遍歷數組和對象。設計模式稱之爲迭代器模式。不少源碼都有相似這樣的遍歷函數。好比你們熟知的jQuery`$.each`。

/**
 * @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
  // 判斷 null 和 undefined 直接返回
  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];
  }

  // 是數組 則用for 循環,調用 fn 函數。參數相似 Array.prototype.forEach 的前三個參數。
  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 in 遍歷對象,但 for in 會遍歷原型鏈上可遍歷的屬性。
    // 因此用 hasOwnProperty 來過濾自身屬性了。
    // 其實也能夠用Object.keys來遍歷,它不遍歷原型鏈上可遍歷的屬性。
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

若是對Object相關的API不熟悉,能夠查看筆者以前寫過的一篇文章。JavaScript 對象全部API解析

第三部分

取消相關API實現,還有allspread、導出等實現。

// Expose Cancel & CancelToken
// 導出 Cancel 和 CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

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

module.exports = axios;

// Allow use of default import syntax in TypeScript
// 也就是能夠如下方式引入
// import axios from 'axios';
module.exports.default = axios;

這裏介紹下spread,取消的API暫時不作分析,後文再詳細分析。

假設你有這樣的需求。

function f(x, y, z) {}
var args = [1, 2, 3];
f.apply(null, args);

那麼能夠用spread方法。用法:

axios.spread(function(x, y, z) {})([1, 2, 3]);

實現也比較簡單。源碼實現:

/**
 * @param {Function} callback
 * @returns {Function}
 */
module.exports = function spread(callback) {
  return function wrap(arr) {
    return callback.apply(null, arr);
  };
};

上文var context = new Axios(defaultConfig);,接下來介紹核心構造函數Axios

核心構造函數 Axios

axios/lib/core/Axios.js

構造函數Axios

function Axios(instanceConfig) {
  // 默認參數
  this.defaults = instanceConfig;
  // 攔截器 請求和響應攔截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Axios.prototype.request = function(config){
  // 省略,這個是核心方法,後文結合例子詳細描述
  // code ...
  var promise = Promise.resolve(config);
  // code ...
  return promise;
}
// 這是獲取 Uri 的函數,這裏省略
Axios.prototype.getUri = function(){}
// 提供一些請求方法的別名
// Provide aliases for supported request methods
// 遍歷執行
// 也就是爲啥咱們能夠 axios.get 等別名的方式調用,並且調用的是 Axios.prototype.request 方法
// 這個也在上面的 axios 結構圖上有所體現。
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
    }));
  };
});

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;

接下來看攔截器部分。

攔截器管理構造函數 InterceptorManager

請求前攔截,和請求後攔截。
Axios.prototype.request函數裏使用,具體怎麼實現的攔截的,後文配合例子詳細講述。

axios github 倉庫 攔截器文檔

如何使用:

// Add a request interceptor
// 添加請求前攔截器
axios.interceptors.request.use(function (config) {
  // Do something before request is sent
  return config;
}, function (error) {
  // Do something with request error
  return Promise.reject(error);
});

// Add a response interceptor
// 添加請求後攔截器
axios.interceptors.response.use(function (response) {
  // Any status code that lie within the range of 2xx cause this function to trigger
  // Do something with response data
  return response;
}, function (error) {
  // Any status codes that falls outside the range of 2xx cause this function to trigger
  // Do something with response error
  return Promise.reject(error);
});

若是想把攔截器,能夠用eject方法。

const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);

攔截器也能夠添加自定義的實例上。

const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});

源碼實現:

構造函數,handles用於存儲攔截器函數。

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

接下來聲明瞭三個方法:使用、移除、遍歷。

InterceptorManager.prototype.use 使用

傳遞兩個函數做爲參數,數組中的一項存儲的是{fulfilled: function(){}, rejected: function(){}}。返回數字ID,用於移除攔截器。

/**
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} 返回ID 是爲了用 eject 移除
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

InterceptorManager.prototype.eject 移除

根據use返回的ID移除 攔截器。

/**
 * @param {Number} id The ID that was returned by `use`
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

有點相似定時器setTimeoutsetInterval,返回值是id。用clearTimeoutclearInterval來清除定時器。

// 提一下 定時器回調函數是能夠傳參的,返回值 timer 是數字
var timer = setInterval((name) => {
  console.log(name);
}, 1000, '若川');
console.log(timer); // 數字 ID
// 在控制檯等會再輸入執行這句,定時器就被清除了
clearInterval(timer);

InterceptorManager.prototype.forEach 遍歷

遍歷執行全部攔截器,傳遞一個回調函數(每個攔截器函數做爲參數)調用,被移除的一項是null,因此不會執行,也就達到了移除的效果。

/**
 * @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);
    }
  });
};

實例結合

上文敘述的調試時運行npm start是用axios/sandbox/client.html路徑的文件做爲示例的,讀者能夠自行調試。

如下是一段這個文件中的代碼。

axios(options)
.then(function (res) {
  response.innerHTML = JSON.stringify(res.data, null, 2);
})
.catch(function (res) {
  response.innerHTML = JSON.stringify(res.data, null, 2);
});

先看調用棧流程

若是不想一步步調試,有個偷巧的方法。
知道axios使用了XMLHttpRequest
能夠在項目中搜索:new XMLHttpRequest
定位到文件axios/lib/adapters/xhr.js
在這條語句var request = new XMLHttpRequest();
chrome瀏覽器中 打個斷點調試下,再根據調用棧來細看具體函數等實現。

Call Stack

dispatchXhrRequest (xhr.js:19)
xhrAdapter (xhr.js:12)
dispatchRequest (dispatchRequest.js:60)
Promise.then (async)
request (Axios.js:54)
wrap (bind.js:10)
submit.onclick ((index):138)

簡述下流程:

  1. Send Request按鈕點擊submit.onclick
  2. 調用axios函數其實是調用Axios.prototype.request函數,而這個函數使用bind返回的一個名爲wrap的函數。
  3. 調用Axios.prototype.request
  4. (有請求攔截器的狀況下執行請求攔截器),中間會執行dispatchRequest方法
  5. dispatchRequest以後調用adapter (xhrAdapter)
  6. 最後調用Promise中的函數dispatchXhrRequest,(有響應攔截器的狀況下最後會再調用響應攔截器)

若是仔細看了文章開始的axios 結構關係圖,其實對這個流程也有大概的瞭解。

接下來看Axios.prototype.request具體實現。

Axios.prototype.request 請求核心方法

這個函數是核心函數。 主要作了這幾件事:

1.判斷第一個參數是字符串,則設置 url,也就是支持 axios('example/url', [, config]),也支持 axios({})
2.合併默認參數和用戶傳遞的參數
3.設置請求的方法,默認是是 get方法
4.將用戶設置的請求和響應攔截器、發送請求的 dispatchRequest組成 Promise鏈,最後返回仍是 Promise實例。
也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序。
也就是爲啥最後仍是能夠 thencatch方法的緣故。
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  // 這一段代碼 其實就是 使 axios('example/url', [, config])
  // config 參數能夠省略
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 合併默認參數和用戶傳遞的參數
  config = mergeConfig(this.defaults, config);

  // Set config.method
  // 設置 請求方法,默認 get 。
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  // Hook up interceptors middleware
  // 組成`Promise`鏈 這段拆開到後文再講述
};

組成Promise鏈,返回Promise實例

這部分:用戶設置的請求和響應攔截器、發送請求的 dispatchRequest組成 Promise鏈。也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序
也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序
也就是爲啥最後仍是能夠 thencatch方法的緣故。

若是讀者對Promise不熟悉,建議讀阮老師的書籍《ES6 標準入門》。阮一峯老師 的 ES6 Promise-resolveJavaScript Promise迷你書(中文版)

// 組成`Promise`鏈
  // Hook up interceptors middleware
  // 把 xhr 請求 的 dispatchRequest 和 undefined 放在一個數組裏
  var chain = [dispatchRequest, undefined];
  // 建立 Promise 實例
  var promise = Promise.resolve(config);

 // 遍歷用戶設置的請求攔截器 放到數組的 chain 前面
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

 // 遍歷用戶設置的響應攔截器 放到數組的 chain 後面
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

 // 遍歷 chain 數組,直到遍歷 chain.length 爲 0
  while (chain.length) {
    // 兩兩對應移出來 放到 then 的兩個參數裏。
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
var promise = Promise.resolve(config);

解釋下這句。做用是生成Promise實例。

var promise = Promise.resolve({name: '若川'})
// 等價於
// new Promise(resolve => resolve({name: '若川'}))
promise.then(function (config){
  console.log(config)
});
// {name: "若川"}

一樣解釋下後文會出現的Promise.reject(error);

Promise.reject(error);
var promise = Promise.reject({name: '若川'})
// 等價於
// new Promise(reject => reject({name: '若川'}))

// promise.then(null, function (config){
//   console.log(config)
// });
// 等價於
promise.catch(function (config){
  console.log(config)
});
// {name: "若川"}

接下來結合例子,來理解這段代碼。
很遺憾,在example文件夾沒有攔截器的例子。筆者在example中在example/get的基礎上添加了一個攔截器的示例。axios/examples/interceptors,便於讀者調試。

node ./examples/server.js -p 5000

promise = promise.then(chain.shift(), chain.shift());這段代碼打個斷點。

會獲得這樣的這張圖。

request方法中promise鏈

特別關注下,右側,local中的chain數組。也就是這樣的結構。

var chain = [
  '請求成功攔截2', '請求失敗攔截2',  
  '請求成功攔截1', '請求失敗攔截1',  
  dispatch,  undefined,
  '響應成功攔截1', '響應失敗攔截1',
  '響應成功攔截2', '響應失敗攔截2',
]

這段代碼相對比較繞。也就是會生成以下相似的代碼,中間會調用dispatchRequest方法。

// config 是 用戶配置和默認配置合併的
var promise = Promise.resolve(config);
promise.then('請求成功攔截2', '請求失敗攔截2')
.then('請求成功攔截1', '請求失敗攔截1')
.then(dispatchRequest, undefined)
.then('響應成功攔截1', '響應失敗攔截1')
.then('響應成功攔截2', '響應失敗攔截2')

.then('用戶寫的業務處理函數')
.catch('用戶寫的報錯業務處理函數');

這裏提下promise`thencatch`知識:
Promise.prototype.then方法的第一個參數是resolved狀態的回調函數,第二個參數(可選)是rejected狀態的回調函數。因此是成對出現的。
Promise.prototype.catch方法是.then(null, rejection).then(undefined, rejection)的別名,用於指定發生錯誤時的回調函數。
then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。

結合上述的例子更詳細一點,代碼則是這樣的。

var promise = Promise.resolve(config);
// promise.then('請求成功攔截2', '請求失敗攔截2')
promise.then(function requestSuccess2(config) {
  console.log('------request------success------2');
  return config;
}, function requestError2(error) {
  console.log('------response------error------2');
  return Promise.reject(error);
})

// .then('請求成功攔截1', '請求失敗攔截1')
.then(function requestSuccess1(config) {
  console.log('------request------success------1');
  return config;
}, function requestError1(error) {
  console.log('------response------error------1');
  return Promise.reject(error);
})

// .then(dispatchRequest, undefined)
.then( function dispatchRequest(config) {
  /**
    * 適配器返回的也是Promise 實例
      adapter = function xhrAdapter(config) {
            return new Promise(function dispatchXhrRequest(resolve, reject) {})
      }
    **/
  return adapter(config).then(function onAdapterResolution(response) {
    // 省略代碼 ...
    return response;
  }, function onAdapterRejection(reason) {
    // 省略代碼 ...
    return Promise.reject(reason);
  });
}, undefined)

// .then('響應成功攔截1', '響應失敗攔截1')
.then(function responseSuccess1(response) {
  console.log('------response------success------1');
  return response;
}, function responseError1(error) {
  console.log('------response------error------1');
  return Promise.reject(error);
})

// .then('響應成功攔截2', '響應失敗攔截2')
.then(function responseSuccess2(response) {
  console.log('------response------success------2');
  return response;
}, function responseError2(error) {
  console.log('------response------error------2');
  return Promise.reject(error);
})

// .then('用戶寫的業務處理函數')
// .catch('用戶寫的報錯業務處理函數');
.then(function (response) {
  console.log('哈哈哈,終於獲取到數據了', response);
})
.catch(function (err) {
  console.log('哎呀,怎麼報錯了', err);
});

仔細看這段Promise鏈式調用,代碼都相似。then方法最後返回的參數,就是下一個then方法第一個參數。
catch錯誤捕獲,都返回Promise.reject(error),這是爲了便於用戶catch時能捕獲到錯誤。

舉個例子:

var p1 = new Promise((resolve, reject) => {
 reject(new Error({name: '若川'}));
});

p1.catch(err => {
    console.log(res, 'err');
    return Promise.reject(err)
})
.catch(err => {
 console.log(err, 'err1');
})
.catch(err => {
 console.log(err, 'err2');
});

err2不會捕獲到,也就是不會執行,但若是都返回了return Promise.reject(err),則能夠捕獲到。

最後畫個圖總結下Promise鏈式調用。

axios promise 鏈式調用

小結:1. 請求和響應的攔截器能夠寫 Promise
2. 若是設置了多個請求響應器,後設置的先執行。
3. 若是設置了多個響應攔截器,先設置的先執行。

dispatchRequest(config)這裏的config是請求成功攔截器返回的。接下來看dispatchRequest函數。

dispatchRequest 最終派發請求

這個函數主要作了以下幾件事情:

1.若是已經取消,則 throw緣由報錯,使 Promise走向 rejected
2.確保 config.header存在。
3.利用用戶設置的和默認的請求轉換器轉換數據。
4.拍平 config.header
5.刪除一些 config.header
6.返回適配器 adapterPromise實例)執行後 then執行後的 Promise實例。返回結果傳遞給響應攔截器處理。
'use strict';
// utils 工具函數
var utils = require('./../utils');
// 轉換數據
var transformData = require('./transformData');
// 取消狀態
var isCancel = require('../cancel/isCancel');
// 默認參數
var defaults = require('../defaults');

/**
 * 拋出 錯誤緣由,使`Promise`走向`rejected`
 */
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 * @returns {Promise} The Promise to be fulfilled
 */
module.exports = function dispatchRequest(config) {
  // 取消相關
  throwIfCancellationRequested(config);

  // Ensure headers exist
  // 確保 headers 存在
  config.headers = config.headers || {};

  // Transform request data
  // 轉換請求的數據
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // Flatten headers
  // 拍平 headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // 如下這些方法 刪除 headers
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );
  // adapter 適配器部分 拆開 放在下文講
};

dispatchRequest 之 transformData 轉換數據

上文的代碼裏有個函數transformData,這裏解釋下。其實就是遍歷傳遞的函數數組 對數據操做,最後返回數據。

axios.defaults.transformResponse數組中默認就有一個函數,因此使用concat連接自定義的函數。

使用:

文件路徑axios/examples/transform-response/index.html

這段代碼其實就是對時間格式的字符串轉換成時間對象,能夠直接調用getMonth等方法。

var ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z/;
function formatDate(d) {
  return (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear();
}

axios.get('https://api.github.com/users/mzabriskie', {
  transformResponse: axios.defaults.transformResponse.concat(function (data, headers) {
    Object.keys(data).forEach(function (k) {
      if (ISO_8601.test(data[k])) {
        data[k] = new Date(Date.parse(data[k]));
      }
    });
    return data;
  })
})
.then(function (res) {
  document.getElementById('created').innerHTML = formatDate(res.data.created_at);
});

源碼:

就是遍歷數組,調用數組裏的傳遞dataheaders參數調用函數。

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

  return data;
};

dispatchRequest 之 adapter 適配器執行部分

適配器,在設計模式中稱之爲適配器模式。講個生活中簡單的例子,你們就容易理解。

咱們經常使用之前手機耳機孔都是圓孔,而如今基本是耳機孔和充電接口合二爲一。統一爲typec

這時咱們須要須要一個typec轉圓孔的轉接口,這就是適配器。

// adapter 適配器部分
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    // 轉換響應的數據
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      // 取消相關
      throwIfCancellationRequested(config);

      // Transform response data
      // 轉換響應的數據
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });

接下來看具體的adapter

adapter 適配器 真正發送請求

var adapter = config.adapter || defaults.adapter;

看了上文的adapter,能夠知道支持用戶自定義。好比能夠經過微信小程序wx.request按照要求也寫一個adapter
接着來看下defaults.ddapter
文件路徑:axios/lib/defaults.js

根據當前環境引入,若是是瀏覽器環境引入xhr,是node環境則引入http
相似判斷node環境,也在sentry-javascript源碼中有看到。

function getDefaultAdapter() {
  var adapter;
  // 根據 XMLHttpRequest 判斷
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
    // 根據 process 判斷
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}
var defaults = {
  adapter: getDefaultAdapter(),
  // ...
};

xhr

接下來就是咱們熟悉的XMLHttpRequest對象。

可能讀者不瞭解能夠參考XMLHttpRequest MDN 文檔

主要提醒下:onabort是請求取消事件,withCredentials是一個布爾值,用來指定跨域Access-Control請求是否應帶有受權信息,如cookie或受權header頭。

這塊代碼有刪減,具體能夠看若川的axios-analysis倉庫,也能夠克隆筆者的axios-analysis倉庫調試時再具體分析。

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 這塊代碼有刪減
    var request = new XMLHttpRequest();
    request.open()
    request.timeout = config.timeout;
    // 監聽 state 改變
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }
      // ...
    }
    // 取消
    request.onabort = function(){};
    // 錯誤
    request.onerror = function(){};
    // 超時
    request.ontimeout = function(){};
    // cookies 跨域攜帶 cookies 面試官常喜歡考這個
    // 一個布爾值,用來指定跨域 Access-Control 請求是否應帶有受權信息,如 cookie 或受權 header 頭。
    // Add withCredentials to request if needed
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // 上傳下載進度相關
    // Handle progress if needed
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    // Send the request
    // 發送請求
    request.send(requestData);
  });
}

而實際上如今fetch支持的很好了,阿里開源的umi-request請求庫,就是用fetch封裝的,而不是用XMLHttpRequest。 文章末尾,大概講述下umi-requestaxios的區別。

http

http這裏就不詳細敘述了,感興趣的讀者能夠自行查看,若川的axios-analysis倉庫

module.exports = function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
  });
};

上文dispatchRequest有取消模塊,我以爲是重點,因此放在最後來細講:

dispatchRequest 之 取消模塊

可使用cancel token取消請求。

axios cancel token API 是基於撤銷的promise取消提議。

The axios cancel token API is based on the withdrawncancelable promises proposal.

axios 文檔 cancellation

文檔上詳細描述了兩種使用方式。

很遺憾,在example文件夾也沒有取消的例子。筆者在example中在example/get的基礎上添加了一個取消的示例。axios/examples/cancel,便於讀者調試。

node ./examples/server.js -p 5000

request中的攔截器和dispatch中的取消這兩個模塊相對複雜,能夠多調試調試,吸取消化。

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/get/server', {
  cancelToken: source.token
}).catch(function (err) {
  if (axios.isCancel(err)) {
    console.log('Request canceled', err.message);
  } else {
    // handle error
  }
});

// cancel the request (the message parameter is optional)
// 取消函數。
source.cancel('哎呀,我被若川取消了');

取消請求模塊代碼示例

結合源碼取消流程大概是這樣的。這段放在代碼在axios/examples/cancel-token/index.html

參數的config.cancelToken是觸發了source.cancel('哎呀,我被若川取消了');才生成的。

// source.cancel('哎呀,我被若川取消了');
// 點擊取消時纔會 生成 cancelToken 實例對象。
// 點擊取消後,會生成緣由,看懂了這段在看以後的源碼,可能就好理解了。
var config = {
  name: '若川',
  // 這裏簡化了
  cancelToken: {
        promise: new Promise(function(resolve){
            resolve({ message: '哎呀,我被若川取消了'})
        }),
        reason: { message: '哎呀,我被若川取消了' }
  },
};
// 取消 拋出異常方法
function throwIfCancellationRequested(config){
  // 取消的狀況下執行這句
  if(config.cancelToken){
    //   這裏源代碼 便於執行,我改爲具體代碼
    // config.cancelToken.throwIfRequested();
    // if (this.reason) {
    //     throw this.reason;
    //   }
    if(config.cancelToken.reason){
        throw config.cancelToken.reason;
    }
  }
}

function dispatchRequest(config){
  // 有多是執行到這裏就取消了,因此拋出錯誤會被err2 捕獲到
  throwIfCancellationRequested(config);
  //  adapter xhr適配器
  return new Promise((resovle, reject) => {
    var request = new XMLHttpRequest();
    console.log('request', request);
    if (config.cancelToken) {
        // Handle cancellation
        config.cancelToken.promise.then(function onCanceled(cancel) {
            if (!request) {
                return;
            }

            request.abort();
            reject(cancel);
            // Clean up request
            request = null;
        });
    }
  })
  .then(function(res){
    // 有多是執行到這裏就才取消 取消的狀況下執行這句
    throwIfCancellationRequested(config);
    console.log('res', res);
    return res;
  })
  .catch(function(reason){
    // 有多是執行到這裏就才取消 取消的狀況下執行這句
    throwIfCancellationRequested(config);
    console.log('reason', reason);
    return Promise.reject(reason);
  });
}

var promise = Promise.resolve(config);

// 沒設置攔截器的狀況下是這樣的
promise
.then(dispatchRequest, undefined)
// 用戶定義的then 和 catch
.then(function(res){
  console.log('res1', res);
  return res;
})
.catch(function(err){
  console.log('err2', err);
  return Promise.reject(err);
});
// err2 {message: "哎呀,我被若川取消了"}

接下來看取消模塊的源碼

看如何經過生成config.cancelToken

文件路徑:

axios/lib/cancel/CancelToken.js

const CancelToken = axios.CancelToken;
const source = CancelToken.source();
source.cancel('哎呀,我被若川取消了');

由示例看CancelToken.source的實現,

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  // token
  return {
    token: token,
    cancel: cancel
  };
};

執行後source的大概結構是這樣的。

{
    token: {
    promise: new Promise(function(resolve){
      resolve({ message: '哎呀,我被若川取消了'})
    }),
    reason: { message: '哎呀,我被若川取消了' }
  },
  cancel: function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      // 已經取消
      return;
    }
    token.reason = {message: '哎呀,我被若川取消了'};
  }
}

接着看new CancelToken

// CancelToken
// 經過 CancelToken 來取消請求操做
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

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

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      // 已經取消
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

module.exports = CancelToken;

發送請求的適配器裏是這樣使用的。

// xhr
if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

dispatchRequest中的throwIfCancellationRequested具體實現:throw 拋出異常。

// 拋出異常函數
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}
// 拋出異常 用戶 { message: '哎呀,我被若川取消了' }
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

取消流程調用棧

1.source.cancel()
2.resolvePromise(token.reason);
3.config.cancelToken.promise.then(function onCanceled(cancel) {})

最後進入request.abort();`reject(cancel);`

到這裏取消的流程就介紹完畢了。主要就是經過傳遞配置參數cancelToken,取消時纔會生成cancelToken,判斷有,則拋出錯誤,使Promise走向rejected,讓用戶捕獲到消息{message: '用戶設置的取消信息'}。

文章寫到這裏就基本到接近尾聲了。

能讀到最後,說明你已經超過不少人啦^_^

axios是很是優秀的請求庫,但確定也不能知足全部開發者的需求,接下來對比下其餘庫,看看其餘開發者有什麼具體需求。

對比其餘請求庫

KoAjax

FCC成都社區負責人水歌開源的KoAJAX

如何用開源軟件辦一場技術大會?如下這篇文章中摘抄的一段。

前端請求庫 —— KoAJAX 國內前端同窗最經常使用的 HTTP 請求庫應該是 axios 了吧?雖然它的 Interceptor(攔截器)API 是 .use(),但和 Node.js 的 Express、Koa 等框架的中間件模式徹底不一樣,相比 jQuery .ajaxPrefilter()、dataFilter() 並沒什麼實質改進;上傳、下載進度比 jQuery.Deferred() 還簡陋,只是兩個專門的回調選項。因此,它仍是要對特定的需求記憶特定的 API,不夠簡潔。

幸運的是,水歌在研究如何用 ES 2018 異步迭代器實現一個類 Koa 中間件引擎的過程當中,作出了一個更有實際價值的上層應用 —— KoAJAX。它的整個執行過程基於 Koa 式的中間件,並且它本身就是一箇中間件調用棧。除了 RESTful API 經常使用的 .get()、.post()、.put()、.delete() 等快捷方法外,開發者就只需記住 .use() 和 next(),其它都是 ES 標準語法和 TS 類型推導。

umi-request 阿里開源的請求庫

umi-request github 倉庫

umi-requestfetch,axios異同。
codeumi-request/code 與 codefetch/code, codeaxios/code 異同

不得不說,umi-request確實強大,有興趣的讀者能夠閱讀下其源碼。

看懂axios的基礎上,看懂umi-request源碼應該不難。

好比umi-request取消模塊代碼幾乎與axios如出一轍。

總結

文章詳細介紹了axios調試方法。詳細介紹了axios構造函數,攔截器,取消等功能的實現。最後還對比了其餘請求庫。

最後畫個圖總結一下axios的整體大體流程。

axios的整體大體流程

解答下文章開頭提的問題:

若是你是求職者,項目寫了運用了axios,面試官可能會問你:

1.爲何 axios既能夠當函數調用,也能夠當對象使用,好比 axios({})axios.get
答: axios本質是函數,賦值了一些別名方法,好比 getpost方法,可被調用,最終調用的仍是 Axios.prototype.request函數。
2.簡述 axios調用流程。
答:實際是調用的 Axios.prototype.request方法,最終返回的是 promise鏈式調用,實際請求是在 dispatchRequest中派發的。
3.有用過攔截器嗎?原理是怎樣的?
答:用過,用 axios.interceptors.request.use添加請求成功和失敗攔截器函數,用 axios.interceptors.response.use添加響應成功和失敗攔截器函數。在 Axios.prototype.request函數組成 promise鏈式調用時, Interceptors.protype.forEach遍歷請求和響應攔截器添加到真正發送請求 dispatchRequest的兩端,從而作到請求前攔截和響應後攔截。攔截器也支持用 Interceptors.protype.eject方法移除。
4.有使用 axios的取消功能嗎?是怎麼實現的?
答:用過,經過傳遞 config配置 cancelToken的形式,來取消的。判斷有傳 cancelToken,在 promise鏈式調用的 dispatchRequest拋出錯誤,在 adapterrequest.abort()取消請求,使 promise走向 rejected,被用戶捕獲取消信息。
5.爲何支持瀏覽器中發送請求也支持 node發送請求?
答: axios.defaults.adapter默認配置中根據環境判斷是瀏覽器仍是 node環境,使用對應的適配器。適配器支持自定義。

回答面試官的問題,讀者也能夠根據本身的理解,組織語言,筆者的回答只是作一個參考。

axios源碼相對很少,打包後一千多行,比較容易看完,很是值得學習。

建議clone若川的 axios-analysis github 倉庫,按照文中方法本身調試,印象更深入。

基於Promise,構成Promise鏈,巧妙的設置請求攔截,發送請求,再試試響應攔截器。

request中的攔截器和dispatch中的取消這兩個模塊相對複雜,能夠多調試調試,吸取消化。

axios既是函數,是函數時調用的是Axios.prototype.request函數,又是對象,其上面有getpost等請求方法,最終也是調用Axios.prototype.request函數。

axios源碼中使用了挺多設計模式。好比工廠模式、迭代器模式、適配器模式等。若是想系統學習設計模式,通常比較推薦豆瓣評分9.1的JavaScript設計模式與開發實踐

若是讀者發現有不妥或可改善之處,再或者哪裏沒寫明白的地方,歡迎評論指出。另外以爲寫得不錯,對您有些許幫助,能夠點贊、評論、轉發分享,也是對筆者的一種支持,很是感謝呀。

推薦閱讀

官方axios github 倉庫

寫文章前,搜索瞭如下幾篇文章泛讀了一下。有興趣再對比看看如下這幾篇,有代碼調試的基礎上,看起來也快。

一直以爲多搜索幾篇文章看,對本身學習知識更有用。有個詞語叫主題閱讀。大概意思就是一個主題一系列閱讀。

@叫我小明呀:Axios 源碼解析
@尼庫尼庫桑:深刻淺出 axios 源碼
@小賊先生_ronffy:Axios源碼深度剖析 - AJAX新王者
逐行解析Axios源碼
[譯]axios 是如何封裝 HTTP 請求的
知乎@Lee : TypeScript 重構 Axios 經驗分享

筆者另外一個系列

面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符

關於

做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
若川的博客,使用vuepress重構了,閱讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault前端視野專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star^_^~

歡迎加微信交流 微信公衆號

可能比較有趣的微信公衆號,長按掃碼關注。歡迎加筆者微信lxchuan12(註明來源,基原本者不拒),拉您進【前端視野交流羣】,長期交流學習~

若川視野

相關文章
相關標籤/搜索