學習axios源碼整體架構,打造屬於自己的請求庫

NO IMAGE

1. 前言

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

學習源碼整體架構系列文章如下:

1.學習 jQuery 源碼整體架構,打造屬於自己的 js 類庫
2.學習 underscore 源碼整體架構,打造屬於自己的函數式編程類庫
3.學習 lodash 源碼整體架構,打造屬於自己的函數式編程類庫
4.學習 sentry 源碼整體架構,打造屬於自己的前端異常監控SDK
5.學習 vuex 源碼整體架構,打造屬於自己的狀態管理庫
6.學習 axios 源碼整體架構,打造屬於自己的請求庫
7.學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理
8.學習 redux 源碼整體架構,深入理解 redux 及其中間件原理

感興趣的讀者可以點擊閱讀。下一篇可能是vue-router源碼。

本文比較長,手機上閱讀,可以直接看文中的幾張圖即可。建議點贊或收藏後在電腦上閱讀,按照文中調試方式自己調試或許更容易吸收消化。

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

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

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

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

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

諸如這類問題。

2. chrome 和 vscode 調試 axios 源碼方法

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

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

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

2.1 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

2.2 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的文件可調試。

3. 先看 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

接下來看具體源碼的實現。可以跟著斷點調試一下。

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

4. 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');

4.1 lib/axios.js主文件

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

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

4.1.1 第一部分

引入一些工具函數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');

4.1.2 第二部分

是生成實例對象 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。接下來講述這幾個工具方法。

4.1.3 工具方法之 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 '若川'

4.1.4 工具方法之 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 調用。

4.1.5 工具方法之 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解析

4.1.6 第三部分

取消相關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

4.2 核心構造函數 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;

接下來看攔截器部分。

4.3 攔截器管理構造函數 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 = [];
}

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

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

4.3.2 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);

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

5. 實例結合

上文敘述的調試時運行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);
});

5.1 先看調用棧流程

如果不想一步步調試,有個偷巧的方法。
知道 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 具體實現。

5.2 Axios.prototype.request 請求核心方法

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

1.判斷第一個參數是字符串,則設置 url,也就是支持axios('example/url', [, config]),也支持axios({})
2.合併默認參數和用戶傳遞的參數
3.設置請求的方法,默認是是get方法
4.將用戶設置的請求和響應攔截器、發送請求的dispatchRequest組成Promise鏈,最後返回還是Promise實例。

也就是保證了請求前攔截器先執行,然後發送請求,再響應攔截器執行這樣的順序。<br>
也就是為啥最後還是可以`then`,`catch`方法的緣故。<br>
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`鏈 這段拆開到後文再講述
};

5.2.1 組成Promise鏈,返回Promise實例

這部分:用戶設置的請求和響應攔截器、發送請求的dispatchRequest組成Promise鏈。也就是保證了請求前攔截器先執行,然後發送請求,再響應攔截器執行這樣的順序

也就是保證了請求前攔截器先執行,然後發送請求,再響應攔截器執行這樣的順序<br>
也就是為啥最後還是可以`then`,`catch`方法的緣故。<br>

如果讀者對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());這段代碼打個斷點。

會得到這樣的這張圖。
學習axios源碼整體架構,打造屬於自己的請求庫

特別關注下,右側,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源碼整體架構,打造屬於自己的請求庫

小結:1. 請求和響應的攔截器可以寫Promise
2. 如果設置了多個請求響應器,後設置的先執行。
3. 如果設置了多個響應攔截器,先設置的先執行。

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

5.3 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 適配器部分 拆開 放在下文講
};

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

5.3.2 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

5.4 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 有取消模塊,我覺得是重點,所以放在最後來細講:

5.5 dispatchRequest 之 取消模塊

可以使用cancel token取消請求。

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

The axios cancel token API is based on the withdrawn cancelable 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('哎呀,我被若川取消了');

5.5.1 取消請求模塊代碼示例

結合源碼取消流程大概是這樣的。這段放在代碼在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: "哎呀,我被若川取消了"}

5.5.2 接下來看取消模塊的源碼

看如何通過生成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是非常優秀的請求庫,但肯定也不能滿足所有開發者的需求,接下來對比下其他庫,看看其他開發者有什麼具體需求。

6. 對比其他請求庫

6.1 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 類型推導。

6.2 umi-request 阿里開源的請求庫

umi-request github 倉庫

umi-requestfetch, axios 異同。

學習axios源碼整體架構,打造屬於自己的請求庫

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

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

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

7. 總結

文章詳細介紹了 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^_^~

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

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

相關文章

駁《慎用trycatch》

頭條:如何獲取瀏覽器指紋信息

學習redux源碼整體架構,深入理解redux及其中間件原理

若川的2019年度總結,波瀾不驚