Axios 源碼解析-完整篇

背景
日常開發(fā)中我們經(jīng)常跟接口打交道,而在現(xiàn)代標(biāo)準(zhǔn)前端框架(Vue/React)開發(fā)中,離不開的是 axios,出于好奇閱讀了一下源碼。
閱讀源碼免不了枯燥無味,容易被上下文互相依賴的關(guān)系搞得一頭露水,我們可以抓住主要矛盾,忽略次要矛盾,可結(jié)合 debugger 調(diào)試模式,先把主干流程梳理清楚,在慢慢啃細(xì)節(jié)比較好,以下是對(duì)源碼和背后的設(shè)計(jì)思想進(jìn)行解讀,不足之處請(qǐng)多多指正。
axios 是什么
基于
promise封裝的http請(qǐng)求庫(避免回調(diào)地獄)支持瀏覽器端和
node端豐富的配置項(xiàng):數(shù)據(jù)轉(zhuǎn)換器,攔截器等等
客戶端支持防御
XSRF生態(tài)完善(支持
Vue/React,周邊插件等等)
另外兩條數(shù)據(jù)證明 axios 使用之廣泛
1.截至 2021 年 6月底,github 的 star 數(shù)高達(dá) 85.4k

2.npm 的周下載量達(dá)到千萬級(jí)別

Axios 的基本使用

源碼目錄結(jié)構(gòu)
先看看目錄說明,如下

執(zhí)行流程
先看看整體執(zhí)行流程,有大體的概念,后面會(huì)細(xì)說
整體流程有以下幾點(diǎn):
axios.create創(chuàng)建單獨(dú)實(shí)例,或直接使用axios實(shí)例(axios/axios.get…)request方法是入口,axios/axios.get等調(diào)用都會(huì)走進(jìn)request進(jìn)行處理請(qǐng)求攔截器
請(qǐng)求數(shù)據(jù)轉(zhuǎn)換器,對(duì)傳入的參數(shù)
data和header做數(shù)據(jù)處理,比如JSON.stringify(data)適配器,判斷是瀏覽器端還是
node端,執(zhí)行不同的方法響應(yīng)數(shù)據(jù)轉(zhuǎn)換器,對(duì)服務(wù)端的數(shù)據(jù)進(jìn)行處理,比如
JSON.parse(data)響應(yīng)攔截器,對(duì)服務(wù)端數(shù)據(jù)做處理,比如
token失效退出登陸,報(bào)錯(cuò)dialog提示返回?cái)?shù)據(jù)給開發(fā)者

入口文件(lib/axios.js)
從下面這段代碼可以得出,導(dǎo)出的 axios 就是實(shí)例化后的對(duì)象,還在其上掛載 create 方法,以供創(chuàng)建獨(dú)立實(shí)例,從而達(dá)到實(shí)例之間互不影響,互相隔離。
...
// 創(chuàng)建實(shí)例過程的方法
function createInstance(defaultConfig) {
return instance;
}
// 實(shí)例化
var axios = createInstance(defaults);
// 創(chuàng)建獨(dú)立的實(shí)例,隔離作用域
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
...
// 導(dǎo)出實(shí)例
module.exports = axios;
可能大家對(duì) createInstance 方法感到好奇,下面一探究竟。
function createInstance(defaultConfig) {
// 實(shí)例化,創(chuàng)建一個(gè)上下文
var context = new Axios(defaultConfig);
// 平時(shí)調(diào)用的 get/post 等等請(qǐng)求,底層都是調(diào)用 request 方法
// 將 request 方法的 this 指向 context(上下文),形成新的實(shí)例
var instance = bind(Axios.prototype.request, context);
// Axios.prototype 上的方法 (get/post...)掛載到新的實(shí)例 instance 上,
// 并且將原型方法中 this 指向 context
utils.extend(instance, Axios.prototype, context);
// Axios 屬性值掛載到新的實(shí)例 instance 上
// 開發(fā)中才能使用 axios.default/interceptors
utils.extend(instance, context);
return instance;
}
從上面代碼可以看得出,Axios 不是簡單的創(chuàng)建實(shí)例 context,而且進(jìn)行一系列的上下文綁定和屬性方法掛載,從而去支持 axios(),也支持 axios.get() 等等用法;
createInstance 函數(shù)是一個(gè)核心入口,我們?cè)诎焉厦媪鞒淌崂硪幌拢?/p>
通過構(gòu)造函數(shù)
Axios創(chuàng)建實(shí)例context,作為下面request方法的上下文(this指向)將
Axios.prototype.request方法作為實(shí)例使用,并把this指向context,形成新的實(shí)例instance將構(gòu)造函數(shù)
Axios.prototype上的方法掛載到新的實(shí)例instance上,然后將原型各個(gè)方法中的this指向context,開發(fā)中才能使用axios.get/post…等等將構(gòu)造函數(shù)
Axios的實(shí)例屬性掛載到新的實(shí)例instance上,我們開發(fā)中才能使用下面屬性axios.default.baseUrl = 'https://…'axios.interceptors.request.use(resolve,reject)
大家可能對(duì)上面第 2 點(diǎn) request 方法感到好奇,createInstance 方法明明可以寫一行代碼 return new Axios() 即可,為什么大費(fèi)周章使用 request 方法綁定新實(shí)例,其實(shí)就只是為了支持 axios() 寫法,開發(fā)者可以寫少幾行代碼。。。
默認(rèn)配置(lib/defaults.js)
從 createInstance 方法調(diào)用發(fā)現(xiàn)有個(gè)默認(rèn)配置,主要是內(nèi)置的屬性和方法,可對(duì)其進(jìn)行覆蓋
var defaults = {
...
// 請(qǐng)求超時(shí)時(shí)間,默認(rèn)不超時(shí)
timeout: 0,
// 請(qǐng)求數(shù)據(jù)轉(zhuǎn)換器
transformRequest: [function transformRequest(data, headers) {...}],
// 響應(yīng)數(shù)據(jù)轉(zhuǎn)換器
transformResponse: [function transformResponse(data) {...}],
...
};
...
module.exports = defaults;
構(gòu)造函數(shù) Axios(lib/core/Axios.js)
主要有兩點(diǎn):
配置:外部傳入,可覆蓋內(nèi)部默認(rèn)配置
攔截器:實(shí)例后,開發(fā)者可通過
use方法注冊(cè)成功和失敗的鉤子函數(shù),比如axios.interceptors.request.use((config)=>config,(error)=>error);
function Axios(instanceConfig) {
// 配置
this.defaults = instanceConfig;
// 攔截器實(shí)例
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
在看看原型方法 request 做了什么
支持多類型傳參
配置優(yōu)先級(jí)定義
通過
promise鏈?zhǔn)秸{(diào)用,依次順序執(zhí)行
// 偽代碼
Axios.prototype.request = function request(config) {
// 為了支持 request(url, {...}), request({url, ...})
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 配置優(yōu)先級(jí): 調(diào)用方法的配置 > 實(shí)例化axios的配置 > 默認(rèn)配置
// 舉個(gè)例子,類似:axios.get(url, {}) > axios.create(url, {}) > 內(nèi)部默認(rèn)設(shè)置
config = mergeConfig(this.defaults, config);
// 攔截器(請(qǐng)求和響應(yīng))
var requestInterceptorChain = [{
fulfilled: interceptor.request.fulfilled,
rejected: interceptor.request.rejected
}];
var responseInterceptorChain = [{
fulfilled: interceptor.response.fulfilled,
rejected: interceptor.response.rejected
}];
var promise;
// 形成一個(gè) promise 鏈條的數(shù)組
var chain = [].concat(requestInterceptorChain, chain, responseInterceptorChain);
// 傳入配置
promise = Promise.resolve(config);
// 形成 promise 鏈條調(diào)用
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
...
return promise;
};
通過對(duì)數(shù)組的遍歷,形成一條異步的 promise 調(diào)用鏈,是 axios 對(duì) promise 的巧妙運(yùn)用,用一張圖表示

攔截器 (lib/core/InterceptorManager.js)
上面說到的 promise 調(diào)用鏈,里面涉及到攔截器,攔截器比較簡單,掛載一個(gè)屬性和三個(gè)原型方法
handler: 存放
use注冊(cè)的回調(diào)函數(shù)use: 注冊(cè)成功和失敗的回調(diào)函數(shù)
eject: 刪除注冊(cè)過的函數(shù)
forEach: 遍歷回調(diào)函數(shù),一般內(nèi)部使用多,比如:
promise調(diào)用鏈那個(gè)方法里,循環(huán)遍歷回調(diào)函數(shù),存放到promise調(diào)用鏈的數(shù)組中
function InterceptorManager() {
// 存放 use 注冊(cè)的回調(diào)函數(shù)
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
// 注冊(cè)成功和失敗的回調(diào)函數(shù)
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
...
});
return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
// 刪除注冊(cè)過的函數(shù)
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
InterceptorManager.prototype.forEach = function forEach(fn) {
// 遍歷回調(diào)函數(shù),一般內(nèi)部使用多
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
dispatchRequest(lib/core/dispatchRequest.js)
上面說到的 promise 調(diào)用鏈中的 dispatchRequest 方法,主要做了以下操作:
transformRequest: 對(duì)
config中的data進(jìn)行加工,比如對(duì)post請(qǐng)求的data進(jìn)行字符串化(JSON.stringify(data))adapter:適配器,包含瀏覽器端
xhr和node端的httptransformResponse: 對(duì)服務(wù)端響應(yīng)的數(shù)據(jù)進(jìn)行加工,比如
JSON.parse(data)
dispatchRequest 局部圖

module.exports = function dispatchRequest(config) {
...
// transformRequest 方法,上下文綁定 config,對(duì) data 和 headers 進(jìn)行加工
config.data = transformData.call(
config, // 上下文環(huán)境,即 this 指向
config.data, // 請(qǐng)求 body 參數(shù)
config.headers, // 請(qǐng)求頭
config.transformRequest // 轉(zhuǎn)換數(shù)據(jù)方法
);
// adapter 是一個(gè)適配器,包含瀏覽器端 xhr 和 node 端的 http
// 內(nèi)置有 adapter,也可外部自定義去發(fā)起 ajax 請(qǐng)求
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// transformResponse 方法,上下文綁定 config,對(duì) data 和 headers 進(jìn)行加工
response.data = transformData.call(
config, // 上下文環(huán)境,即 this 指向
response.data, // 服務(wù)端響應(yīng)的 data
response.headers, // 服務(wù)端響應(yīng)的 headers
config.transformResponse // 轉(zhuǎn)換數(shù)據(jù)方法
);
return response;
}, function onAdapterRejection(reason) {
...
return Promise.reject(reason);
});
};
數(shù)據(jù)轉(zhuǎn)換器(lib/core/transformData.js)
上面說到的數(shù)據(jù)轉(zhuǎn)換器,比較好理解,源碼如下
module.exports = function transformData(data, headers, fns) {
var context = this || defaults;
// fns:一個(gè)數(shù)組,包含一個(gè)或多個(gè)方法轉(zhuǎn)換器方法
utils.forEach(fns, function transform(fn) {
// 綁定上下文 context,傳入 data 和 headers 參數(shù)進(jìn)行加工
data = fn.call(context, data, headers);
});
return data;
};
fns 方法即(請(qǐng)求或響應(yīng))數(shù)據(jù)轉(zhuǎn)換器方法,在剛開始 defaults 文件里定義的默認(rèn)配置,也可外部自定義方法,源碼如下:
Axios(lib/defaults.js)
var defaults = {
...
transformRequest: [function transformRequest(data, headers) {
// 對(duì)外部傳入的 headers 進(jìn)行規(guī)范糾正,比如 (accept | ACCEPT) => Accept
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
...
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
// post/put/patch 請(qǐng)求攜帶 data,需要設(shè)置頭部 Content-Type
setContentTypeIfUnset(headers, 'application/json');
// 字符串化
return JSON.stringify(data);
}
return data;
}],
transformResponse: [function transformResponse(data) {
...
try {
// 字符串解析為 json
return JSON.parse(data);
} catch (e) {
...
}
return data;
}],
}
可以看得出,(請(qǐng)求或響應(yīng))數(shù)據(jù)轉(zhuǎn)換器方法是存放在數(shù)組里,可定義多個(gè)方法,各司其職,通過遍歷器對(duì)數(shù)據(jù)進(jìn)行多次加工,有點(diǎn)類似于 node 的管道傳輸 src.pipe(dest1).pipe(dest2)
適配器(lib/defaults.js)
主要包含兩部分源碼,即瀏覽器端 xhr 和 node 端的 http 請(qǐng)求,通過判斷環(huán)境,執(zhí)行不同端的 api。
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 瀏覽器
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// node
adapter = require('./adapters/http');
}
return adapter;
}
對(duì)外提供統(tǒng)一 api,但底層兼容瀏覽器端和 node 端,類似 sdk,底層更改不影響上層 api,保持向后兼容
發(fā)起請(qǐng)求(lib/adapters/xhr.js)
平時(shí)用得比較多的是瀏覽器端,這里只講 XMLHttpRequest 的封裝,node 端有興趣的同學(xué)自行查看源碼(lib/adapters/http.js)
簡易版流程圖表示大致內(nèi)容:

源碼比較長,使用偽代碼表示重點(diǎn)部分
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
...
// 初始化一個(gè) XMLHttpRequest 實(shí)例對(duì)象
var request = new XMLHttpRequest();
// 拼接url,例如:https://www.baidu,com + /api/test
var fullPath = buildFullPath(config.baseURL, config.url);
// 初始化一個(gè)請(qǐng)求,拼接url,例如:https://www.baidu,com/api/test + ?a=10&b=20
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 超時(shí)斷開,默認(rèn) 0 永不超時(shí)
request.timeout = config.timeout;
// 當(dāng) readyState 屬性發(fā)生變化時(shí)觸發(fā),readyState = 4 代表請(qǐng)求完成
request.onreadystatechange = resolve;
// 取消請(qǐng)求觸發(fā)該事件
request.onabort = reject;
// 一般是網(wǎng)絡(luò)問題觸發(fā)該事件
request.onerror = reject;
// 超時(shí)觸發(fā)該事件
request.ontimeout = reject;
// 標(biāo)準(zhǔn)瀏覽器(有 window 和 document 對(duì)象)
if (utils.isStandardBrowserEnv()) {
// 非同源請(qǐng)求,需要設(shè)置 withCredentials = true,才會(huì)帶上 cookie
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// request對(duì)象攜帶 headers 去請(qǐng)求
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
// data 為 undefined 時(shí),移除 content-type,即不是 post/put/patch 等請(qǐng)求
delete requestHeaders[key];
} else {
request.setRequestHeader(key, val);
}
});
}
// 取消請(qǐng)求,cancelToken 從外部傳入
if (config.cancelToken) {
// 等待一個(gè) promise 響應(yīng),外部取消請(qǐng)求即執(zhí)行
config.cancelToken.promise.then(function onCanceled(cancel) {
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
// 發(fā)送請(qǐng)求
request.send(requestData);
});
};
取消請(qǐng)求(lib/cancel/CancelToken.js)
先看看 axios 中文文檔使用
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 處理錯(cuò)誤
}
});
// 取消請(qǐng)求(message 參數(shù)是可選的)
source.cancel('Operation canceled by the user.');
可以猜想,CancelToken 對(duì)象掛載有 source 方法,調(diào)用 source 方法返回 {token, cancel},調(diào)用函數(shù) cancel 可取消請(qǐng)求,但 axios 內(nèi)部怎么知道取消請(qǐng)求,只能通過 { cancelToken: token } ,那 token 跟 cancel 必然有某種聯(lián)系
看看源碼這段話
CancelToken掛載source方法用于創(chuàng)建自身實(shí)例,并且返回{token, cancel}token是構(gòu)造函數(shù)CancelToken的實(shí)例,cancel方法接收構(gòu)造函數(shù)CancelToken內(nèi)部的一個(gè)cancel函數(shù),用于取消請(qǐng)求創(chuàng)建實(shí)例中,有一步是創(chuàng)建處于
pengding狀態(tài)的promise,并掛在實(shí)例方法上,外部通過參數(shù)cancelToken將實(shí)例傳遞進(jìn)axios內(nèi)部,內(nèi)部調(diào)用cancelToken.promise.then等待狀態(tài)改變當(dāng)外部調(diào)用方法
cancel取消請(qǐng)求,pendding狀態(tài)就變?yōu)?resolve,即取消請(qǐng)求并且拋出reject(message)
function CancelToken(executor) {
var resolvePromise;
/**
* 創(chuàng)建處于 pengding 狀態(tài)的 promise,將 resolve 存放在外部變量 resolvePromise
* 外部通過參數(shù) { cancelToken: new CancelToken(...) } 傳遞進(jìn) axios 內(nèi)部,
* 內(nèi)部調(diào)用 cancelToken.promise.then 等待狀態(tài)改變,當(dāng)外部調(diào)用方法 cancel 取消請(qǐng)求,
* pendding 狀態(tài)就變?yōu)?nbsp;resolve,即取消請(qǐng)求并且拋出 reject(message)
*/
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
// 保留 this 指向,內(nèi)部可調(diào)用
var token = this;
executor(function cancel(message) {
if (token.reason) {
// 取消過的直接返回
return;
}
// 外部調(diào)用 cancel 取消請(qǐng)求方法,Cancel 實(shí)例化,保存 message 并增加已取消請(qǐng)求標(biāo)示
// new Cancel(message) 后等于 { message, __CANCEL__ : true}
token.reason = new Cancel(message);
// 上面的 promise 從 pedding 轉(zhuǎn)變?yōu)?nbsp;resolve,并攜帶 message 傳遞給 then
resolvePromise(token.reason);
});
}
// 掛載靜態(tài)方法
CancelToken.source = function source() {
var cancel;
/**
* 構(gòu)造函數(shù) CancelToken 實(shí)例化,用回調(diào)函數(shù)做參數(shù),并且回調(diào)函數(shù)
* 接收 CancelToken 內(nèi)部的函數(shù) c,保存在變量 cancel 中,
* 后面調(diào)用 cancel 即取消請(qǐng)求
*/
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
總結(jié)
上述分析概括成以下幾點(diǎn):
為了支持
axios()簡潔寫法,內(nèi)部使用request函數(shù)作為新實(shí)例使用
promsie鏈?zhǔn)秸{(diào)用的巧妙方法,解決順序調(diào)用問題數(shù)據(jù)轉(zhuǎn)換器方法使用數(shù)組存放,支持?jǐn)?shù)據(jù)的多次傳輸與加工
適配器通過兼容瀏覽器端和
node端,對(duì)外提供統(tǒng)一api取消請(qǐng)求這塊,通過外部保留
pendding狀態(tài),控制promise的執(zhí)行時(shí)機(jī)
參考文獻(xiàn)
Github Axios 源碼(https://github.com/axios/axios)
Axios 文檔說明(http://www.axios-js.com/zh-cn/docs)
一步一步解析Axios源碼,從入門到原理(https://blog.csdn.net/qq_27053493/article/details/97462300
點(diǎn)個(gè)『在看』支持下 
