requireJs的模塊加載和依賴機制的分析和簡單實現。


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的詳解,最后發現自己墨水有限,好多東西只可意會不能言傳,最后放棄了。   如果關於這篇文章大家有什么好的意見和建議,請與我討論,我們一起來完善這篇文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM