第三課:sea.js模塊加載原理

模塊加載,其實就是把js分紅不少個模塊,便於開發和維護。所以加載不少js模塊的時候,須要動態的加載,以便提升用戶體驗。node

在介紹模塊加載庫以前,先介紹一個方法。jquery

動態加載js方法:面試

function loadJs(url , callback){數組

  var node = document.createElement("script");瀏覽器

      node[window.addEventListener ? "onload":"onreadystatechange"] = function(){閉包

            if(window.addEventListener || /loaded|complete/i.test(node.readyState)){app

      callback();jsp

      node.onreadystatechange = null;模塊化

    }                                                                             函數

  }

  node.onerror = function(){};

     node.src = url;

  var head = document.getElementsByTagName("head")[0];

  head.insertBefore(node,head.firstChild);     //插入到head的第一個節點前,防止ie6下head標籤沒閉合前,使用appendChild報錯。 

}

 

我來說下sea.js的模塊加載過程吧:

頁面chaojidan.jsp,在head標籤中,引入sea.js,這時就會獲得seajs對象。

同時引入index.js。

index.js的代碼以下:

seajs.use(['./a','jquery'],function(a,$){ var num = a.a; $('#J_A').text(num); })

a.js :

define(function(require,exports,module){ var b = require('./b'); var a = function(){ return 1 + parseInt(b.b()); } exports.a = a; })

b.js :

define(function(require,exports,module){ var c = require('./c'); var b = function(){ return 2 + parseInt(c.c()); } exports.b = b; })

c.js :

define(function(require,exports,module){ var c = function(){ return 3; } exports.c = c; })

由上可知,a模塊依賴b,b依賴c.

當程序進入到index.js,seajs將調用use方法。


seajs.use = function(ids, callback) {
  globalModule._use(ids, callback)
}
說明: globalModule 爲seajs初始化時(引入sea.js時),Module的實例 
var globalModule = new Module(util.pageUri, STATUS.COMPILED)

此時 ids -> ['./a','jquery'], callback -> function(a,$){var num = a.a;$('#J_A').text(num);}

 

接下來將調用 globalModule._use(ids, callback)

Module.prototype._use = function(ids, callback) {  
  var uris = resolve(ids, this.uri); //解析['./a','jquery'] this._load(uris, function() { //把解析出來的a,jquery模塊的地址[url1,url2],調用_load方法。           //util.map : 讓數據成員所有執行一次一個指定的函數,並返回一個新的數組,該數組爲原數組成員執行回調後的結果 var args = util.map(uris, function(uri) { return uri ? cachedModules[uri]._compile() : null;//若是存在url,就調用_compile方法。
   })
   if (callback) { callback.apply(null, args) } 
  })

   }

由於調用_load方法後,會出現兩個回調函數,所以咱們將function(a,$){var num = a.a;$('#J_A').text(num);}標誌爲callback1,
this._load(uris, function() { })回調方法標誌爲callback2.
resolve方法就是解析模塊地址的,這裏我就不細講了。

最終var uris = resolve(ids, this.uri)中 的uris被解析成了['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js'],模塊路徑解析已經完畢。

而接下來將執行this._load

// _load()方法主要會先判斷哪些資源文件尚未ready,若是所有資源文件都處於ready狀態就執行callback2 // 在這其中還會作循環依賴的判斷,以及對沒有加載的js執行加載 Module.prototype._load = function(uris, callback2) {  
  //util.filter : 讓數據成員所有執行一次一個指定的函數,並返回一個新的數組,該數組爲原數組成員執行回調後返回爲true的成員 //unLoadedUris是那些沒有被編譯的模塊uri數組 var unLoadedUris = util.filter(uris, function(uri) { //返回執行函數布爾值爲true的成員,在uri存在而且在內部變量cacheModules中不存在或者它在存儲信息中status的值小於STATUS.READY時返回true // STATUS.READY值爲4,小於四則可能的狀況是獲取中,下載中。 return uri && (!cachedModules[uri] || cachedModules[uri].status < STATUS.READY) });  
  //若是uris中的模塊所有都ready好了,執行回調並退出函數體(這時就會調用模塊的_compile方法了)。
  var length = unLoadedUris.length
  if (length === 0) { callback2() return }
  //還未加載的模塊個數 var remain = length  //建立閉包,嘗試去加載那些沒有加載的模塊 for (var i = 0; i < length; i++) { (function(uri) {  //判斷若是在內部變量cachedModules裏面並不存在該uri的存儲信息則實例化一個Module對象 var module = cachedModules[uri] || (cachedModules[uri] = new Module(uri, STATUS.FETCHING))  //若是模塊的狀態值大於等於2,也就意味着模塊已經被下載好並已經存在於本地了,這個時候執行onFetched() //不然則調用fetch(uri, onFetched) ,嘗試下載資源文件,資源文件下載後會觸發onload,onload中會執行回調onFetched的方法。 module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched) function onFetched() { module = cachedModules[uri]  //當模塊的狀態值爲大於等於STATUS.SAVED的時候,也就意味着該模塊全部的依賴信息已經被拿到 if (module.status >= STATUS.SAVED) {  //getPureDependencies:獲得不存在循環依賴的依賴數組 var deps = getPureDependencies(module)  //若是依賴數組不爲空 if (deps.length) {  //再次執行_load()方法,直到所有依賴加載完成後執行回調 Module.prototype._load(deps, function() { cb(module) }) }  //若是依賴數組爲空的狀況下,直接執行cb(module) else { cb(module) } }  // 若是獲取失敗後,好比404或者不符合模塊化規範 //在這種情形下,module.status會維持在 FETCHING 或者 FETCHED else { cb() } } })(unLoadedUris[i]) }  // cb 方法 - 加載完全部模塊執行回調 function cb(module) {  // 若是module的存儲信息存在,那麼修改它的module存儲信息中的status的值,修改成 STATUS.READY module && (module.status = STATUS.READY)  // 只有當全部模塊加載完畢後執行回調。 --remain === 0 && callback2() } }
}

這裏unLoadedUris的數組長度爲2 ,['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js'],因此 接下來會產生兩個以 js路徑爲名稱的閉包。

以http://localhost/test/SEAJS/a.js爲例 
接下來 : 首先會建立一個Module:

cachedModules('http://localhost/test/SEAJS/a.js') = new Module('http://localhost/test/SEAJS/a.js',1)

module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched)

由於此時a模塊並無加載 因此接下來將會執行 fetch(uri, onFetched) 即fetch('http://localhost/test/SEAJS/a.js',onFetched)。

function fetch(uri, onFetched) {  // 根據map中的規則替換uri爲新的請求地址 var requestUri = util.parseMap(uri)  // 首先在已獲取列表中查找是否含有requestUri記錄 if (fetchedList[requestUri]) {  // 這個時候將原始uri的module存儲信息刷新到經過map重定義的requestUri上 cachedModules[uri] = cachedModules[requestUri]  // 執行onFetched 並返回,意味着模塊已經獲取成功了 onFetched() return } //在獲取列表中查詢 requestUri 的存儲信息 if (fetchingList[requestUri]) {  //在callbacklist中加入該uri對應下的callback,並返回 callbackList[requestUri].push(onFetched) //若是正在獲取中,就把此模塊的onFetched回調方法push進數組中,並返回。 return }  // 若是嘗試獲取的模塊都未出如今fetchedList和fetchingList中,則分別在請求列表和回調列表中添加其信息 fetchingList[requestUri] = true callbackList[requestUri] = [onFetched]  // Fetches it Module._fetch( requestUri, function() { fetchedList[requestUri] = true  // Updates module status // 若是 module.status 等於 STATUS.FECTCHING ,則修改module狀態爲FETCHED var module = cachedModules[uri] if (module.status === STATUS.FETCHING) { module.status = STATUS.FETCHED } if (fetchingList[requestUri]) { delete fetchingList[requestUri] }  // Calls callbackList 統一執行回調 if (callbackList[requestUri]) { util.forEach(callbackList[requestUri], function(fn) { fn()  //fn就是模塊a對應的onFeched方法。 }) delete callbackList[requestUri] } }, config.charset ) }
接下來 將會執行 Module._fetch(),這裏的回調函數咱們稱做爲callback3.

Module._fetch(),這裏的回調函數咱們稱做爲callback3.

此方法就是調用loadJs方法動態下載a.js文件。(由於有a和jquery,因此會新建兩個script),這裏有一個疑問,新建a的script,並添加到head中,就會下載js文件,可是在seajs中,並無下載,而是等jquery的script創建好,並添加到head中,纔會下載(谷歌調試器設斷點,一直顯示pending等待中)。這是爲毛?
(推薦看這裏:http://ux.sohu.com/topics/50972d9ae7de3e752e0081ff,這裏我說下額外的問題,你們可能知道爲何咱們要少用table來佈局,由於table在呈現樹佈局的時候,須要屢次計算,而div只須要一次。同時,美的電商面試官告訴我:table須要所有解析完纔會顯示出來,而div解析多少就顯示多少。經查證table中若是有tbody標籤,就會按照tbody來分段顯示。所以在IE6,7,8中,若是你用innerHTML來建立一個"<table></table>",會自動在裏面添加<tbody></tbody>。)。
下載成功後,就會解析執行,執行的是define方法。這裏會先執行a模塊的代碼。
define(id,deps,function(){})方法解析

此方法就是調用loadJs方法動態下載a.js文件。(由於有a和jquery,因此會新建兩個script),這裏有一個疑問,新建a的script,並添加到head中,就會下載js文件,可是在seajs中,並無下載,而是等jquery的script創建好,並添加到head中,纔會下載(谷歌調試器設斷點,一直顯示pending等待中)。這是爲毛?
(推薦看這裏:http://ux.sohu.com/topics/50972d9ae7de3e752e0081ff,這裏我說下額外的問題,你們可能知道爲何咱們要少用table來佈局,由於table在呈現樹佈局的時候,須要屢次計算,而div只須要一次。同時,美的電商面試官告訴我:table須要所有解析完纔會顯示出來,而div解析多少就顯示多少。經查證table中若是有tbody標籤,就會按照tbody來分段顯示。所以在IE6,7,8中,若是你用innerHTML來建立一個"<table></table>",會自動在裏面添加<tbody></tbody>。)。
下載成功後,就會解析執行,執行的是define方法。這裏會先執行a模塊的代碼。
define(id,deps,function(){})方法解析

//define 定義 ,id : 模塊id , deps : 模塊依賴 , factory  Module._define = function(id, deps, factory) {

   //解析依賴關係 // 若是deps不是數組類型,同時factory是函數

   if (!util.isArray(deps) && util.isFunction(factory)) { // 函數體內正則匹配require字符串,並造成數組返回賦值給deps

     deps = util.parseDependencies(factory.toString())

   }

  //設置元信息

   var meta = { id: id, dependencies: deps, factory: factory } 

 

   if (document.attachEvent) {

     // 獲得當前script的節點

     var script = util.getCurrentScript()

       // 若是script節點存在

     if (script) {

         // 獲得原始uri地址

         derivedUri = util.unParseMap(util.getScriptAbsoluteSrc(script)) }

         if (!derivedUri) {

             util.log('Failed to derive URI from interactive script for:', factory.toString(), 'warn')

         }

     }

 .........

 

 }
}

  define首先會對factory執行一個判斷 ,判斷它是否爲一個函數(緣由是由於define內也能夠包括文件,對象)

 

若是是函數 , 那麼 就會經過factory.toString(),獲得函數,並經過正則匹配得 a.js的依賴,並把依賴保存在 deps 中

 

對於 a.js 而言, 它的依賴 是 b.js 因此 deps爲 ['./b']

 

並對 a.js 的信息進行保存 var meta = { id: id, dependencies: deps, factory: factory }

 

針對a.js meta = { id : undefined , dependencies : ['./b'] , factory : function(xxx){xxx}}

 

在 ie 6-9 瀏覽器中能夠拿到當前運行js的路徑 可是在標準瀏覽器中 ,這不可行 ,因此暫時先把元信息賦值給anonymousModuleMeta = meta。

 

而後觸發onload,這時就會調用回調方法callback3,此回調方法就會修改當前回調模塊(a.js)的狀態值,將其設置爲 module.status = STATUS.FETCHED。

再接下來 ,將統一 執行回調隊列 callbackList 中的 a.js所對應的回調,也就是onFetched

onFetched方法會檢查a模塊是否有依賴模塊,由於a依賴於b,因此對模塊a所依賴的b.js 執行_load()。

會去下載b模塊,這時會先執行jquery的define方法。由於jquery沒依賴模塊,因此onload回調後。onFetched調用cb方法。

 

 

當b按照a同樣的過程實現後,就會下載c模塊。最終c,b,a模塊都下載執行define,並onload結束後,也會調用cb方法,(先c,再b,後c)

全部模塊都爲ready以後,就會調用callback2方法。
最終回調到callback2,執行a和jquery模塊的最終回調到callback2,執行a和jquery模塊的_compile方法:

首先編譯a.js模塊,模塊a的function執行,由於a裏面有require(b.js),所以會去執行b模塊的function.
模塊 a 的function開始執行
模塊 b 的function開始執行
模塊 c 的function開始執行
模塊 c 的function執行完畢
模塊 b 的function執行完畢
模塊 a 的function執行完畢

最後執行jquery的function。

編譯結束後,就執行callback1,就可使用a和jquery對象了。

seajs版本已經更新,如今沒有_compile方法了。(你們自行去看,我也要去看下)

加油!
相關文章
相關標籤/搜索