axios原始碼的分析,到目前為止,算上第0章已經四章了,但是實際上,還都沒有進入axios真正的主線,我們來簡單回顧下。最開始我們構建了get請求,寫了重要的buildURL方法,然後我們處理請求體請求頭,響應體響應頭,這樣我們就可以傳json物件了,然後還加入了promise,讓我們可以鏈式點用,最後還加了錯誤處理,讓我們可以更好的操作請求資訊。
但是,大家發現了沒有,目前為止我們所寫的核心其實就是一個XMLHttpRequest物件,所有的內容都圍繞著這個物件。程式碼也沒有做太清晰的分割,那麼今天,我們就來完成axios的核心主題,也就是Axios類,有了這個,大家就可以通過一些直觀的方法來快速的呼叫axios的請求API了。
依照慣例,從axios的API入手,我們今天要實現的內容如下:
那麼接下來我們就進入正題吧。
首先,我們在core資料夾下建立一個Axios檔案。宣告一個Axios類:
export default function Axios(config) {}
這個axios很簡單,我們暫時這樣,什麼都不需要。然後我們在Axios的原型上掛載一個request方法,這個方法是真正的請求方法,也就是說,所有的axios請求,其實都是request。
Axios.prototype.request = function (url, config) { if (typeof url === "string") { if (!config) { config = {}; } config.url = url; } else { config = url; } return dispatchRequest(config); };
首先我們來看下,request方法實際上有兩個核心,一個是引數的過載,聽起來很高大上,實際上就是可以傳一個引數,也可以把url單獨抽離出來作為引數及其他的config來傳遞,最重要的就是dispatchRequest,上面說所有的axios的請求都是request,那麼其實request,就是dispatchRequest。我們來看下,怎麼搞出來的dispatchRequest。
還記不記得之前lib根目錄下的axios,沒錯,把裡面的程式碼複製過來就可以了。那axios就沒東西了,我們改下axios裡的程式碼:
import Axios from "./core/Axios"; import bind from "./helpers/bind"; import utils from "./utils"; /** * Create an instance of Axios * * @param {Object} defaultConfig The default config for the instance * @return {Axios} A new instance of Axios */ function createInstance(defaultConfig) { var context = new Axios(defaultConfig); var instance = bind(Axios.prototype.request, context); // Copy axios.prototype to instance utils.extend(instance, Axios.prototype, context); // Copy context to instance utils.extend(instance, context); return instance; } // Create the default instance to be exported var axios = createInstance(); // Expose Axios class to allow class inheritance axios.Axios = Axios; export default axios;
誒?怎麼程式碼風格變了?好吧,我承認這是從axios原始碼複製過來的,毛都沒改,就改了改引用。然後呢,這個createInstance實際上就是個工廠函式。建立並返回axios的例項。我們暫時不看extend和bind具體的原始碼,從字面意思來看,instance例項上繫結request方法,也就是說,我可以直接使用axios.request。extend就是把某些東西,也就是複製了屬性到例項上。OK,到此,核心的axios體系基本上完成了。但是我們還漏了一個很重要的事情,就是本章最開始的呼叫方式,我們希望可以在例項上直接呼叫get、post等方法。那麼我們來看下程式碼:
// Provide aliases for supported request methods utils.forEach( ["delete", "get", "head", "options"], function forEachMethodNoData(method) { /*eslint func-names:0*/ Axios.prototype[method] = function (url, config) { return this.request( mergeConfig(config || {}, { method: method, url: url, data: (config || {}).data, }) ); }; } ); utils.forEach(["post", "put", "patch"], function forEachMethodWithData(method) { /*eslint func-names:0*/ Axios.prototype[method] = function (url, data, config) { return this.request( mergeConfig(config || {}, { method: method, url: url, data: data, }) ); }; });
上面把方法分成了兩類,一類是有body的,一類是沒有的。然後呼叫request,把引數傳進去就好了,簡單的一批。至於mergeConfig方法,我們們稍後面再說。我會盡可能的把他們都註釋一遍,可以去原始碼裡查閱,因為這些東西都差不多可以拆出來,單獨使用,不在axios的核心線上,utils是單純的工具,與業務無關,而helpers包含了對業務的一定的抽象和關聯。到這裡,我們就可以使用axiso.get這樣的方法來呼叫介面了。
那,額外的,我們來分析下bind、extend和mergeConfig方法:
1.bind
我們先來看下程式碼:
export default 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]; } return fn.apply(thisArg, args); }; }
我們們先來字面看一下,傳入了一個fn和thisArg引數,然後返回了一個wrap函式。wrap裡面根據wrap的arguments長度建立了個陣列,然後挨個的把arguments的引數複製給args,然後再返回一個fn.apply。所以,字面意思我們們理解了,但是它到底幹了啥呢?我們就拿用到了它的地方做個舉例:
var context = new Axios(defaultConfig); var instance = bind(Axios.prototype.request, context);
那怎們把引數傳進去,其實他就是:
return Axios.prototype.request.apply(context, args);
解釋下這句話吧,就是request方法的this只想context,也就是new Axios(defaultConfig),然後把args作為引數傳進去,那args就是傳給wrap的引數。那我們再寫個例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> function bind(fn, thisArg) { return function wrap() { console.log(arguments); var args = new Array(arguments.length); for (var i = 0; i < args.length; i++) { args[i] = arguments[i]; } return fn.apply(thisArg, args); }; } function A(config) { console.log(config); } A.prototype.request = function () { console.log("request"); }; var context = new A({ a: 2, b: 3 }); var instance = bind(A.prototype.request, context); instance(1, 2, 3); </script> </html>
大家捋一下哦。我們再回到這塊程式碼:
function createInstance(defaultConfig) { var context = new Axios(defaultConfig); var instance = bind(Axios.prototype.request, context); // Copy axios.prototype to instance utils.extend(instance, Axios.prototype, context); // Copy context to instance utils.extend(instance, context); return instance; }
最後,我們返回了instance,對吧?那麼就意味著我們可以這樣用:
instance({ method:'post', // xxxxxx })
那我有個問題,最終這個傳遞的引數給了誰?如果不知道答案的話,那就再回頭看一遍吧~~~。
2.extend
extend方法,說白了,就是把a的屬性,複製給b沒了。我們來看下程式碼:
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; }
我們來看下,字面意思的話,就是,如果有thisArg,並且某一個b中的屬性是一個函式,那麼a中對應的key就是bind後的函式,否則就是單純的複製。簡單吧~~~。那這裡我就不說bind是咋回事了啊,看不懂回頭看啊。我們們這叫一步三回頭。:)
3.mergeConfig
這個呢,看起來複雜, 說起來簡單,因為篇幅較長,我就不在這裡說了,大家自己去專案中對應的分支看註釋哦。但是我簡單說下,這個mergeConfig實際上使用了一種策略模式,簡單點說其實就是根據不同的物件,來分配不同的合併方法。一共有那麼1、2、3、4、5,哦對,四種合併策略(去看了原始碼你就知道我這裡沒說錯了,我擴起來說是怕你罵我,你罵我倒無所謂,我怕你罵錯了,嘻嘻)。
我們再來回顧下,今天的核心主線:
- 建立Axios類。
- 在Axios的原型上擴充套件核心request方法。
- 擴充套件其他alias方法,內部就是呼叫的request。
- 建立dispatchRequest,是request方法的核心(就是之前舊的lib/axios裡的程式碼)。
- 建立createInstance工廠函式,繫結資料到例項上。
- 解釋了bind、extend方法的含義,mergeConfig自己去程式碼看。
今天就做了這些,其實不復雜,跟著我,帶你一比一還原axios(其實就是教你怎麼抄)。額……咳咳……讀書人的事,怎麼能叫做抄呢~~我們下一章子再抄,哦不,再借鑑噢。