requireJs的文件加載和依賴管理確實非常好用,相信大家都有這個體會。在此之前,我們的html文件頭部總是要有一長串的script標簽來引入js文件,並且還必須非常注意script標簽的先后順序。
這篇文章對requireJs的核心功能做了簡單實現,希望能幫助大家更好理解requireJs.
下面的思路是我參考了requireJs 0.0.7版本實現的。之前有嘗試理解當前版本的requireJs的源碼,不過最后發現,這特么不是短時間能搞的定的。 無奈之下找了github上先前較早的版本,那時還沒有那么多配置項,代碼結構更簡單一點。
--------------------boom----------------
首先,假設我們有這樣一個文件結構
js/require.js
js/main.js
js/a.js js/b.js js/a1.js js/a2.js js/b1.js js/b2.js
index.html
我們的入口文件時main.js, 在入口文件中,我們調用了require函數
require(["a","b"],function(a,b){ // do something. });
我們看到上面的require函數中,回掉函數的執行依賴於a和b兩個模塊
然后我們的a.js文件像這樣
define("a",["a1","a2"],function(a1,b1){ //do something });
可以看到a模塊依賴於a1,a2模塊。
a1模塊像這樣
define("a1",function(){ //do something });
同理b模塊依賴於b1,b2模塊,文件結構類似。
------------------boom--------------
先說說require和define函數的關系。
require和define函數接收同樣的參數,不同的是,define函數被建議在一個文件中使用一次,用它來定義模塊。
require函數一般在入口文件或者頂層代碼中使用,用來加載和使用模塊。
其實在我看來,require函數可以看做是特殊的define函數,它用來定義一個頂層匿名模塊,這個模塊不需要被其他模塊加載。
二者的區別這里有一些介紹 requirejs中define和require的定位以及使用區別?
requireJs中的執行流程
一,requrieJs首先找到data-main屬性,然后根據屬性值(通過新建一個script標簽)加載並且解析入口文件。
下面看入口文件中的 require(["a","b"],function(){})調用發生了什么?
二,在require函數中,我們先生成一個簡單的模塊對象,大概是這樣的
{moduleName:"_@$1",deps:["a","b"],callback:function(){},callbackReturn:null,args:[]}
對這個模塊對象屬性的解釋:
moduleName:模塊名稱。 前面我們說了,require函數可以看做是定義一個匿名的頂層模塊對象。所以這里生成了一個內部名稱"_@$1"
deps: 依賴數組; 包含當前模塊依賴的模塊。
callback:回調函數。 require中的那個回調函數。
callbackReturn:回調函數返回值 (其實貌似這里並不需要這個屬性,我主要考慮到用這個屬性來存儲模塊回調函數的返回值,這樣當我們多次依賴這個模塊時,可以直接返回這個值。)
args:數組,對應於依賴模塊的傳遞回來的值
我們在全局設置一個context對象
context = {}; context.topModule = ""; //存儲requre函數調用生成的頂層模塊對象名。 context.modules = {}; //存儲所有的模塊。使用模塊名作為key,模塊對象作為value context.loaded = []; //加載好的模塊 (加載好是指模塊所在的文件加載成功) context.waiting = [];//等待加載完成的模塊
我們在這里設置
context.topModule = "_@$1"; //因為當前定義的是一個頂層匿名模塊,所以生成一個內部模塊名。
context.modules 中添加_@$1模塊,結果像這樣
{ "_@$1":{moduleName:"_@$1",deps:["a","b"],callback:function(){},callbackReturn:null,args:[]} }
context.waiting 中添加依賴的模塊,結果像這樣 ["a","b"]; //把依賴的模塊添加到waiting中 (其實這里還可以優化為先判斷依賴模塊是否已經存在於context.loaded中)
然后我們遍歷依賴數組 ["a","b"],分別創建script標簽並加載,綁定好data-moduleName屬性,和加載完成回調函數onscriptLoaded ,在遍歷中大概像這樣
var script = document.createElement("script"); script.onload = onscriptLoaded; //腳本加載好后的回調函數。 這是個核心函數 script.setAttribute("data-moduleName","a"); //為script元素添加data-moduleName屬性,方便在回調函數中判斷當前模塊 script.src = "js/a.js"; document.getElementsByTagName("head")[0].appendChild(script);
到這里require函數就完成了。
三。假設上面的js/a.js加載好了,文件中執行了
define("a",["a1","a2"],function(a1,b1){ //do something });
我們看看define中做了哪些事。其實define函數和上面的require函數做了差不多相同的事,差別在於require自動生成了一個模塊名。並且require中設置了context.topModule.
生成模塊 {moduleName:"a",deps:["a1","a2"],callback:function(){},callbackReturn:null,args:[]}
修改全局context變量
context.modules中添加當前模塊 ,結果如下
{
"_@$1":{moduleName:"_@$1",deps:["a","b"],callback:function(){},callbackReturn:null,args:[]},
"a":{moduleName:"a",deps:["a1","a2"],callback:function(){},callbackReturn:null,args:[]} }
context.waiting添加當前依賴數組。 --結果 ["a","b","a1","b1"]
然后接着根據依賴數組創建script標簽,綁定data-moduleName屬性,綁定回調函數onscriptLoaded
四。最后的關鍵函數onscriptLoaded
function onscriptLoaded(event){
思路大概是這樣。
1.根據event對象我們可以得到加載完成的script元素,得到它的data-moduleName屬性,這個屬性就是模塊名
2.在全局context對象中,給 context.loaded數組中加上這個模塊名。 context.waiting數組中減去這個模塊名。
3.接下來判斷, 如果context.waiting數組不為空則返回。
4.否則如果context.waiting為空數組,表明所有的依賴都已經加載了。
接下來就是重頭戲。
5.創建一個遞歸函數來執行模塊回調函數,像這樣
function exec(module) { var deps = module.deps; //當前模塊的依賴數組 var args = module.args; //當前模塊的回調函數參數 for (var i = 0, len = deps.length; i < len; i++) { //遍歷 var dep = context.modules[deps[i]]; args[i] = exec(dep); //遞歸得到依賴模塊返回值作為對應參數 } return module.callback.apply(module, args); // 調用回調函數,傳遞給依賴模塊對應的參數。 } var topModule = context.modules[context.topModule]; //找到頂層模塊。 exec(topModule); //開始執行遞歸函數
} //onscriptLoaded結束
整個實現的思路就是,我們在define和require中定義模塊時,所有的依賴的模塊名都被添加到了context.waiting數組中。 每個依賴在加載時的script標簽都綁定了onload事件,在事件回調函數中我們把當前模塊名從context.waiting中刪除,接着我們判斷context.waiting是否為空,為空時意味着所有模塊的文件都加載好了,此時就可以從頂層模塊開始,使用一個遞歸函數來執行模塊的回調函數。
最后
我本來就只是想寫出一個核心的思路,所以代碼中很多地方還值得琢磨,可能並不正確,但整體的思路沒錯。
注意這里我在使用define函數時,模塊名參數我並沒有省略,這是因為,在本片文章的實現思路中,我並沒有更多的篇幅來解釋怎么來實現define函數的省略模塊名。 大概的思路可能是在define執行時,我們並不知道當前定義的模塊的模塊名,所以我們創建一個臨時的模塊名,然后全局中設置一個變量temp指向這個模塊。 考慮到define函數執行完后,它所在的script標簽的onload事件必然會緊接着觸發,而且這個script標簽上有data-moduleName綁定了正確的模塊名,所以我們可以在onload事件回調函數中找到temp指向的模塊,然后修改它的模塊名。
之前本來准備寫篇關於requireJs api的詳解,最后發現自己墨水有限,好多東西只可意會不能言傳,最后放棄了。 如果關於這篇文章大家有什么好的意見和建議,請與我討論,我們一起來完善這篇文章。