學習 axios 源碼整體架構,打造屬於自己的請求庫
- 2020 年 1 月 2 日
- 筆記
前言
這是
學習源碼整體架構系列
第六篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是程式碼整體結構,不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的程式碼。
學習源碼整體架構系列
文章如下:
1.學習 jQuery 源碼整體架構,打造屬於自己的 js 類庫
2.學習underscore源碼整體架構,打造屬於自己的函數式編程類庫
3.學習 lodash 源碼整體架構,打造屬於自己的函數式編程類庫
4.學習 sentry 源碼整體架構,打造屬於自己的前端異常監控SDK
感興趣的讀者可以點擊閱讀。下一篇可能是vue-router
源碼。
本文比較長,手機上閱讀,可以滑到有圖的地方直接看文中的幾張圖即可。建議點贊或收藏後在電腦上閱讀,按照文中調試方式自己調試或許更容易吸收消化。
導讀 文章詳細介紹了 axios
調試方法。詳細介紹了 axios
構造函數,攔截器,取消等功能的實現。最後還對比了其他請求庫。
本文學習的版本是v0.19.0
。克隆的官方倉庫的master
分支。截至目前(2019 年 12 月 14 日),最新一次commit
是2019-12-09 15:52 ZhaoXC
dc4bc49673943e352
,fix: fix ignore set withCredentials false (#2582)
。
本文倉庫在這裡若川的 axios-analysis github 倉庫。求個star
呀。
如果你是求職者,項目寫了運用了axios
,面試官可能會問你:
1.為什麼
axios
既可以當函數調用,也可以當對象使用,比如axios({})
、axios.get
。 2.簡述axios
調用流程。 3.有用過攔截器嗎?原理是怎樣的? 4.有使用axios
的取消功能嗎?是怎麼實現的? 5.為什麼支援瀏覽器中發送請求也支援node
發送請求? 諸如這類問題。
chrome 和 vscode 調試 axios 源碼方法
前不久,筆者在知乎回答了一個問題一年內的前端看不懂前端框架源碼怎麼辦?推薦了一些資料,閱讀量還不錯,大家有興趣可以看看。主要有四點:
1.藉助調試 2.搜索查閱相關高贊文章 3.把不懂的地方記錄下來,查閱相關文檔 4.總結
看源碼,調試很重要,所以筆者詳細寫下 axios
源碼調試方法,幫助一些可能不知道如何調試的讀者。
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
的結構是怎樣的,先有一個大概印象。
筆者畫了一張比較詳細的圖表示。

看完結構圖,如果看過jQuery
、underscore
和lodash
源碼,會發現其實跟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
文件 程式碼相對比較多。分為三部分展開敘述。
- 第一部分:引入一些工具函數
utils
、Axios
構造函數、默認配置defaults
等。 - 第二部分:是生成實例對象
axios
、axios.Axios
、axios.create
等。 - 第三部分取消相關 API 實現,還有
all
、spread
、導出等實現。
第一部分
引入一些工具函數utils
、Axios
構造函數、默認配置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');
第二部分
是生成實例對象 axios
、axios.Axios
、axios.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
,也就是用戶不需要知道內部是怎麼實現的。 舉個生活的例子,我們買手機,不需要知道手機是怎麼做的,就是工廠模式。 看完第二部分,裡面涉及幾個工具函數,如bind
、extend
。接下來講述這幾個工具方法。
工具方法之 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
這樣的類數組對象了,不需要手動轉數組。 那麼為啥作者要轉數組,為了性能?當時不支援?抑或是作者不知道?這就不得而知了。有讀者知道歡迎評論區告訴筆者呀。
關於apply
、call
和bind
等不是很熟悉的讀者,可以看筆者的另一個面試官問系列
。 面試官問:能否模擬實現 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 實現,還有all
、spread
、導出等實現。
// 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; } };
有點類似定時器setTimeout
和 setInterval
,返回值是id
。用clearTimeout
和clearInterval
來清除定時器。
// 提一下 定時器回調函數是可以傳參的,返回值 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)
簡述下流程:
Send Request
按鈕點擊submit.onclick
- 調用
axios
函數實際上是調用Axios.prototype.request
函數,而這個函數使用bind
返回的一個名為wrap
的函數。 - 調用
Axios.prototype.request
- (有請求攔截器的情況下執行請求攔截器),中間會執行
dispatchRequest
方法 dispatchRequest
之後調用adapter (xhrAdapter)
- 最後調用
Promise
中的函數dispatchXhrRequest
,(有響應攔截器的情況下最後會再調用響應攔截器)
如果仔細看了文章開始的axios 結構關係圖
,其實對這個流程也有大概的了解。
接下來看 Axios.prototype.request
具體實現。
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`鏈 這段拆開到後文再講述 };
組成Promise
鏈,返回Promise
實例
這部分:用戶設置的請求和響應攔截器、發送請求的
dispatchRequest
組成Promise
鏈。也就是保證了請求前攔截器先執行,然後發送請求,再響應攔截器執行這樣的順序
也就是保證了請求前攔截器先執行,然後發送請求,再響應攔截器執行這樣的順序<br> 也就是為啥最後還是可以`then`,`catch`方法的緣故。<br>
如果讀者對Promise
不熟悉,建議讀阮老師的書籍《ES6 標準入門》。阮一峰老師 的 ES6 Promise-resolve 和 JavaScript 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());
這段程式碼打個斷點。
會得到這樣的這張圖。

特別關注下,右側,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
then
和catch
知識: 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
。
- 如果設置了多個請求響應器,後設置的先執行。
- 如果設置了多個響應攔截器,先設置的先執行。
dispatchRequest(config)
這裡的config
是請求成功攔截器返回的。接下來看dispatchRequest
函數。
dispatchRequest 最終派發請求
這個函數主要做了如下幾件事情:
1.如果已經取消,則
throw
原因報錯,使Promise
走向rejected
。 2.確保config.header
存在。 3.利用用戶設置的和默認的請求轉換器轉換數據。 4.拍平config.header
。 5.刪除一些config.header
。 6.返回適配器adapter
(Promise
實例)執行後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); });
源碼:
就是遍曆數組,調用數組裡的傳遞 data
和 headers
參數調用函數。
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-request
和 axios
的區別。
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 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('哎呀,我被若川取消了');
取消請求模組程式碼示例
結合源碼取消流程大概是這樣的。這段放在程式碼在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-request
與 fetch
, axios
異同。

umi-request
與fetch
,axios
異同
不得不說,umi-request
確實強大,有興趣的讀者可以閱讀下其源碼。
看懂axios
的基礎上,看懂umi-request
源碼應該不難。
比如 umi-request
取消模組程式碼幾乎與axios
一模一樣。
總結
文章詳細介紹了 axios
調試方法。詳細介紹了 axios
構造函數,攔截器,取消等功能的實現。最後還對比了其他請求庫。
最後畫個圖總結一下 axios 的總體大致流程。

axios的總體大致流程
解答下文章開頭提的問題:
如果你是求職者,項目寫了運用了axios
,面試官可能會問你:
1.為什麼
axios
既可以當函數調用,也可以當對象使用,比如axios({})
、axios.get
。 答:axios
本質是函數,賦值了一些別名方法,比如get
、post
方法,可被調用,最終調用的還是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
拋出錯誤,在adapter
中request.abort()
取消請求,使promise
走向rejected
,被用戶捕獲取消資訊。 5.為什麼支援瀏覽器中發送請求也支援node
發送請求? 答:axios.defaults.adapter
默認配置中根據環境判斷是瀏覽器還是node
環境,使用對應的適配器。適配器支援自定義。
回答面試官的問題,讀者也可以根據自己的理解,組織語言,筆者的回答只是做一個參考。
axios
源碼相對不多,打包後一千多行,比較容易看完,非常值得學習。
建議 clone
若川的 axios-analysis github 倉庫,按照文中方法自己調試,印象更深刻。
基於Promise
,構成Promise
鏈,巧妙的設置請求攔截,發送請求,再試試響應攔截器。
request
中的攔截器和dispatch
中的取消這兩個模組相對複雜,可以多調試調試,吸收消化。
axios
既是函數,是函數時調用的是Axios.prototype.request
函數,又是對象,其上面有get
、post
等請求方法,最終也是調用Axios.prototype.request
函數。
axios
源碼中使用了挺多設計模式。比如工廠模式、迭代器模式、適配器模式等。如果想系統學習設計模式,一般比較推薦豆瓣評分 9.1 的JavaScript 設計模式與開發實踐
如果讀者發現有不妥或可改善之處,再或者哪裡沒寫明白的地方,歡迎加我微信 lxchuan12 交流。另外覺得寫得不錯,對您有些許幫助,可以點贊、評論、轉發分享,也是對筆者的一種支援,非常感謝呀。