前端要看源碼,就從axios開始吧,由於它的邏輯不只沒有很複雜,並且設計也很巧妙。打包後只有一千六百多行,挑三揀四,蜻蜓點水,去掉註釋後,就更沒多少了。
來句官方的話:Axios 是一個基於 promise 的 HTTP 庫,能夠用在瀏覽器和 node.js 中。javascript
在發起[delete
, get
, head
, options
]請求時,能夠這樣使用:前端
axios.get('/user',{
headers,
data
}).then(data=>{
//do something
})
複製代碼
在發起[post
, put
, patch
]請求時,能夠這樣使用:java
axios.post('/user',data,{
headers,
}).then(data=>{
//do something
})
複製代碼
這種方式發起請求,能夠看到,axios
被看成了一個對象,該對象上擁有許多方法,看起來就是這樣子的:node
let axios = {
delete() { },
get() { },
head() { },
options() { },
post() { },
put() { },
patch() { }
}
複製代碼
發送全部類型請求時,均可以這樣使用:ios
axios('/user',{
method:'get',
headers,
}).then(data=>{
//do something
})
複製代碼
或者json
axios({
url:'/user',
method:'get',
headers,
}).then(data=>{
//do something
})
複製代碼
固然,get
請求能夠簡化成這樣:axios
axios('/user').then(data=>{
//do something
})
複製代碼
這種方式發起請求,能夠看到,axios
被看成了一個函數,看起來是這樣子的:promise
function axios(url,config) {
//do something
}
複製代碼
經過了解axios
的用法,咱們知道了,axios既能夠是一個對象,也能夠是一個函數,看成爲對象時,它身上掛載了不少請求方法,好比:get
、post
、head
等等;看成爲函數時,它能夠直接調用,傳遞配置參數,參數傳遞有兩種形式,分別是axios(url[,config])
、axios(config)
。要作到這種效果,咱們先來了解一下函數。瀏覽器
Javascript中函數是很值得思考的東西,好比說,它有3種角色:app
函數就是一個普通函數,這種角色在常見不過了,也是咱們常用的,好比定義一個函數foo
,而後調用它,能夠是下面這樣:
//定義
function foo(){
//do something
}
//調用
foo()
複製代碼
函數也能夠被看成一個構造函數來使用,好比定義一個構造函數Foo
,而後獲取它的一個實例,能夠是下面這樣:
//定義
function Foo(){
//do something
}
//獲取一個實例
let instance=new Foo()
複製代碼
這裏函數名首字母大寫純粹是爲了向某語言、某標準、某約定俗成看齊,其餘什麼做用都沒有(無知的說這話)
敲黑板,劃重點,函數也能夠被看成一個對象,也就是說,咱們能夠將一個函數做爲對象來使用,給它定義屬性,獲取它的屬性值,此時和它的另一個身份(函數)毫無關係。好比有一個函數foo
,我就不把你當成函數,就把它當成對象,給它定義屬性,獲取它的屬性值,ok,沒毛病。
function foo(){
//do something
}
//給foo定義一個屬性,值爲數字
foo.a=1;
//給foo定義一個屬性,值爲函數
foo.b=function(){};
//獲取foo的屬性a的值
console.log(foo.a)
複製代碼
如今,我又想把foo
當成一個函數對待了,ok,沒毛病,你是導演,你說了算。
//如今把foo當成函數來調用
foo()
複製代碼
瞭解了這些,咱們來看下axios是如何作到多種使用方式的。
源碼以下:
function createInstance(defaultConfig) {
//獲取Axios的一個實例
var context = new Axios(defaultConfig);
//將Axios原型上的request方法的上下文(也就是this)綁定爲剛建立的實例
var instance = bind(Axios.prototype.request, context);
//將Axios原型上的屬性和方法擴展到instance上
utils.extend(instance, Axios.prototype, context);
//將建立的實例上的屬性和方法擴展到instance上
utils.extend(instance, context);
//返回instance
return instance;
}
//axios就是上面的instance
var axios = createInstance(defaults);
//...
module.exports = axios;
複製代碼
上面代碼中,Axios.prototype.request
是一個方法,用工具函數bind
將它的上下文(this)綁定爲Axios的實例,獲得instance
,也就是說instance
是綁定this
後的request
方法,接下來就是圍繞instance
來搞事情了。 instance
自己是一個函數,可是這裏,將它做爲一個對象來處理了,給它身上定義了一些屬性和方法。
將Axios.prototype
上的屬性和方法擴展到了instance
上,Axios.prototype
上有哪些屬性和方法呢,源碼以下:
//定義request方法,該方法是重點,其餘調用方式最終調用的都是這個方法
Axios.prototype.request = function request(config){/* ... */}
//在原型上批量定義方法,這些方法不接收請求體數據,好比get請求
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {/* ... */};
});
//在原型上批量定義方法,這些方法接收請求體數據,好比post請求
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {/* ... */};
});
複製代碼
上面代碼中都是在Axios的原型上定義方法,首先定義request
方法,其次批量定義[delete
, get
, head
, options
]方法,最後批量定義[post
, put
, patch
]方法,也就是說給原型上定義的這些方法最後都會被擴展到instance
上,咱們就能夠這樣使用了:axios.get()
、axios.post()
等等。 其實,不管咱們是將axios
做爲一個對象來調用get
、post
方法,仍是將axios
做爲一個函數來調用,最終調用的都是原型上的request
方法。
瞭解了axios
是如何作到多種使用方式的,接下來看一下axios
中的配置,也就是用戶傳入的配置項是如何走完整個流程的。
axios.get()
等沒有請求體方法傳入的配置當咱們這樣調用axios
時:
axios.get('/user',{headers,timeout,...})
複製代碼
axios
源碼內部會將咱們傳入的配置作一層處理,這層處理很簡單,首先判斷是否傳遞config
,而後將method
、url
合併到config
,最後調用request
方法,並將處理後的配置傳遞給request
。 源碼以下:
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
複製代碼
axios.post()
等有請求體方法傳入的配置當咱們這樣調用axios
時:
axios.post('/user',{name:'wz'},{headers,timeout,...})
複製代碼
axios
內部的處理方式和上面調用get
方式幾乎相同,惟一不一樣的是多處理了一下請求體數據。源碼以下:
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data //處理請求體參數
}));
};
});
複製代碼
Axios.prototype.request
方法中對配置作了什麼axios.get()
、axios.post
、axios()
,不管是哪一種調用方式,最終都會調用request
方法,看一下關於request
方法的源碼:
Axios.prototype.request = function request(config) {
if (typeof config === 'string') {
config = utils.merge({
url: arguments[0]
}, arguments[1]);
}
//...
//這裏能夠看出配置的優先順序,從左至右依次增高
config = utils.merge(defaults, {method: 'get'}, this.defaults, config);
//將請求方法變爲小寫
config.method = config.method.toLowerCase();
//...
};
複製代碼
request
方法先對參數作一次判斷,若是第一個參數類型爲字符串,也就是咱們是這樣使用的,axios('/user',{...})
,會將字符串放到一個新對象上的url
屬性上,而後和第二個參數作依次合併。接下來就是按照從小到大的權重合並多個配置。
defaults
是axios
的默認配置,權重最低。{method:'get'}
是設置默認請求方法。this.defaults
,是新建立axios
實例時傳入的配置對象.axios.create({...})
config
,調用axios
時傳遞的參數,權重最高。到這裏axios
內部對config
的處理就算完成了,接下來,config
會被依次交給請求攔截器去處理,讓咱們看一下axios
中的攔截器吧。
先看一下這3種東西的關係,以下圖:
XMLHttpRequest
,
node
環境使用
http(s).request()
方法;響應攔截器是處理響應數據。
攔截器是一個函數,分爲兩種類型:請求攔截器和響應攔截器,請求攔截器的主要做用是,提供了一種機制,使得咱們能夠集中處理各類請求時傳遞的參數,就比如人們去乘坐地鐵同樣,無論你是從哪裏來的,手裏拿着什麼東西,只要乘坐地鐵都會通過安檢,攔截器就比如這道安檢同樣,每一個安檢有各自的安檢任務,第一個安檢經過後,會轉交到第二個安檢,第三個,直到全部的安檢都經過了,才能抵達候車區。
axios
中關於攔截器的源碼:
function InterceptorManager() {
//存放攔截器的容器
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
//將用戶傳入的攔截器放入容器中,
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
//返回攔截器在容器中的位置,用於取消攔截器
return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
//經過攔截器在容器中的位置,刪除指定的攔截器
this.handlers[id] = null;
}
};
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);//將遍歷到的每個攔截器回傳給外部
}
});
};
複製代碼
攔截器相關的源碼不難理解,InterceptorManager
構造函數原型上定義了攔截器的添加、刪除、遍歷3個方法。須要注意的是,每個攔截器又分爲兩個方法,成功和失敗,這是由於,axios
是基於promise
實現鏈式調用的,因此每個攔截器的成功失敗方法會分別傳遞給promise
的then
方法當中。對promise
不太瞭解的,能夠看這裏
響應攔截器的道理和請求攔截器同樣,只不過響應攔截器攔截的是返回的數據,而請求攔截器攔截的是請求的配置。 接下來就走到轉換器了。
轉換器是一個函數,分爲兩種類型:請求數據轉換器和響應數據轉換器。請求轉換器函數接受2個參數,分別爲data
,headers
,主要是處理請求體和請求頭的。響應數據轉換器主要是處理響應數據的,好比將JSON數據轉爲普通數據。
源碼中默認的轉換器:
let defaults={
//請求數據轉化器
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Content-Type');
//判斷請求體數據類型爲如下任一一種的話,直接返回
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],
//響應數據轉換器
transformResponse: [function transformResponse(data) {
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],
}
複製代碼
上面代碼簡單看看就能夠了,大概知道攔截器在作什麼事情,其實仔細思考能夠發現,轉化器作到事情在攔截器裏一樣可以作到,只不過爲了職責單一,分工明確,將數據處理這部分工做單獨放到轉換器裏來作了。
接下來就到發送請求了,這部分工做是由適配器作的。
適配器會根據當前使用axios
的環境來決定使用哪一種工具去發送請求,當前環境是瀏覽器的話,就會使用XMLHttpRequest
,是node
的話,會使用http(s).request()
。
源碼以下:
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 瀏覽器環境,使用 xhr 適配器
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined') {
// node環境,使用 http 適配器
adapter = require('./adapters/http');
}
return adapter;
}
複製代碼
./adapters/xhr
文件裏,除了對IE進行一些判斷處理外,基本上就是發送AJAX那幾個經典步驟了,以下:
//獲取 XMLHttpRequest 實例
let xhr = new new XMLHttpRequest();
//肯定請求方法等參數
xhr.open(method,url,...);
//監聽請求狀態
xhr.onreadystatechange = function () {
//...
}
//發送請求
xhr.send();
複製代碼
./adapters/http
文件裏,是使用node
的包http
、https
來完成請求的。
axios
內部是如何組織攔截器、轉換器、適配器呢,看一下源碼:
Axios.prototype.request = function request(config) {
//chain裏存放着全部的攔截器、轉換器、適配器
var chain = [dispatchRequest, undefined];
//生成一個成功的promise,數據爲通過一系列處理後的配置
var promise = Promise.resolve(config);
//this.interceptors.request存放着全部的請求攔截器
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
//將全部的請求攔截器依次放入chain容器中,這裏使用的是unshift,
//也就是說,請求攔截器被是從chain容器的頭部開始,依次放入。
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
//this.interceptors.response存放着全部的響應攔截器
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
//將全部的響應攔截器依次放入chain容器中,這裏使用的是push,
//也就是說,響應攔截器被是從chain容器的尾部開始,依次放入。
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
//鏈式調用chain容器中的攔截器、轉換器、適配器,注意這裏使用的是
//chain.shift()方法,就是說取的時候是從chain容器頭部開始,直到尾部,線性順序。
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
複製代碼
chain
容器在初始化的時候就加入了dispatchRequest
和undefined
,接下來咱們看一下dispatchRequest
具體作了什麼
dispatchRequest
都作了什麼從發送一個請求到這裏只剩下這幾件事情了,
dispatchRequest
主要就是作了以上3件事。 簡化後的源碼以下:module.exports = function dispatchRequest(config) {
// 轉化請求數據
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
var adapter = config.adapter || defaults.adapter;
//adapter方法會根據調用請求方法所在的環境來選擇具體的請求方法。
return adapter(config).then(function onAdapterResolution(response) {
// 轉換響應數據
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {/* ... */});
};
複製代碼
回顧一下,從發送一個請求開始,到成功接收到響應,這風風雨雨的一路,全在圖裏了。