首頁 >web前端 >js教程 >javascript框架設計讀書筆記之模組載入系統_javascript技巧

javascript框架設計讀書筆記之模組載入系統_javascript技巧

WBOY
WBOY原創
2016-05-16 16:29:231257瀏覽

模組加載,其實就是把js分成很多個模組,方便開發與維護。因此載入很多js模組的時候,需要動態的載入,以便提高使用者體驗。

在介紹模組載入庫之前,先介紹一個方法。

動態載入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)){
      callback();
      node.onreadystatechange = null;
    }                                                   
  }
  node.onerror = function(){};
     node.src = url;
  var head = document.getElementsByTagName("head")[0];
  head.insertBefore(node,head.firstChild);     //插入到head的第一個節點前,防止ie6下head標籤沒閉合前,使用appendChild報錯。 
}

由於司徒正美使用了它寫的mass框架來介紹模組加載,而業界用的最多的是require.js和sea.js。因此,我覺得他個性有點強。

我來講下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     });    
  //如果uris中的模組全部都ready好了,執行回呼並退出函數體(這時就會呼叫模組的_compile方法了)。
  var length = unLoadedUris.length
  if (length === 0) { callback2() return }
  //還未載入的模組個數
    var remain = length
    //建立閉包,嘗試去載入那些沒有載入的模組
    for (var i = 0; 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.

此方法就是呼叫loadJs方法動態下載a.js檔。 (因為有a和jquery,所以會新建兩個script),這裡有一個疑問,新建a的script,並添加到head中,就會下載js文件,但是在seajs中,並沒有下載,而是等jquery的script建立好,並且加入到head中,才會下載(Google偵錯器設斷點,一直顯示pending等待中)。這是為毛?
(推薦看這裡:http://ux.sohu.com/topics/50972d9ae7de3e752e0081ff,這裡我說下額外的問題,大家可能知道為什麼我們要少用table來佈局,因為table在呈現樹佈局的時候,需要多次計算,而div只需要一次。標籤,就會依照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模組的_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物件了。

PS:seajs版本已經更新,現在沒有_compile方法了。 (大家自行去看,我也要去看)

接著講下seajs的模組編譯_compile過程。

首先是a.js的編譯

複製程式碼 程式碼如下:

Module.prototype._compile = function() {
126     var module = this         
127     // 如果模組已經編譯過,則直接回傳module.exports
128     if (module.status === STATUS.COMPILED) {
129       return module.exports
130     }
133     //  1. the module file is 404.
134     //  2. the module file is not written with valid module format.
135     //  3. other error cases.
136     // 這裡是處理一些異常狀況,此時直接回傳null
137     if (module.status 138       return null
139     }
140     // 變更模組狀態為COMPILING,表示模組正在編譯
141     module.status = STATUS.COMPILING
142
143     // 模組內部使用,是一種方法,用來獲取其他模組提供(稱為子模組)的接口,同步操作
144     function require(id) {
145         // 依id解析模組的路徑
146         var uri = resolve(id, module.uri)
147         // 從模組快取取得模組(注意,其實這裡子模組作為主模組的依賴項是已經下載下來的)
148         var child = cachedModules[uri]
149
150         // Just return null when uri is invalid.
151         // 若child為空,只能表示參數填入錯誤導致uri不正確,那麼直接回傳null
152         if (!child) {
153           return null
154         }
155
156         // Avoids circular calls.
157         // 若子模組的狀態為STATUS.COMPILING,直接返回child.exports,避免因為循環依賴反覆編譯模組
158         if (child.status === STATUS.COMPILING) {
159           return child.exports
160         }
161         // 指向初始化時呼叫目前模組的模組。根據此屬性,可以得到模組初始化時的Call Stack.
162         child.parent = module
163         // 回傳編譯過的child的module.exports
164         return child._compile()
165     }
166     // 模組內部使用,用來非同步載入模組,並在載入完成後執行指定回呼。
167     require.async = function(ids, callback) {
168       module._use(ids, callback)
169     }
170     // 使用模組系統內部的路徑解析機制來解析並返回模組路徑。函數不會載入模組,只傳回解析後的絕對路徑。
171     require.resolve = function(id) {
172       return resolve(id, module.uri)
173     }
174     // 透過此屬性,可以檢視到模組系統所載入過的所有模組。
175     // 在某些情況下,如果需要重新載入某個模組,可以得到該模組的 uri, 然後透過 delete require.cache[uri] 來將其資訊刪除掉。這樣下次          使用時,就會重新取得。
176     require.cache = cachedModules
177
178     // require是一種方法,用來取得其他模組所提供的介面。
179     module.require = require
180     // exports是一個對象,用來向外提供模組介面。
181     module.exports = {}
182     var factory = module.factory
183
184     // factory 為函數時,表示模組的建構方法。執行該方法,可以得到模組向外提供的介面。
185     if (util.isFunction(factory)) {
186       compileStack.push(module)
187       runInModuleContext(factory, module)
188       compileStack.pop()
189     }
190     // factory 為物件、字串等非函數型別時,表示模組的介面就是該物件、字串等值。
191     // 如:define({ "foo": "bar" });
192     // 如:define('I am a template. My name is {{name}}.');
193     else if (factory !== undefined) {
194       module.exports = factory
195     }
196
197     // 變更模組狀態為COMPILED,表示模組已編譯
198     module.status = STATUS.COMPILED
199     // 執行模組介面修改,透過seajs.modify()
200     execModifiers(module)
201     return module.exports
202   }

複製程式碼 程式碼如下:

if (util.isFunction(factory)) {
186       compileStack.push(module)
187       runInModuleContext(factory, module)
188       compileStack.pop()
189     }

這裡就是把module.export進行初始化。 runInModuleContext方法:

複製程式碼 程式碼如下:

// 依照模組上下文執行模組程式碼
489   function runInModuleContext(fn, module) {
490     // 傳入與模組相關的兩個參數以及模組本身
491     // exports用來暴露介面
492     // require用來取得依賴模組(同步)(編譯)
493     var ret = fn(module.require, module.exports, module)
494     // 支援回傳值暴露介面形式,如:
495     // return {
496     //   fn1 : xx
497     //   ,fn2 : xx
498     //   ...
499     // }
500     if (ret !== undefined) {
501       module.exports = ret
502     }
503   }

執行a.js中的function方法,這時會呼叫var b = require("b.js"),
require方法會傳回b的compile方法的回傳值,b模組中又有var c = require('c.js')。
這時會呼叫c的compile方法,然後呼叫c的function,c中,如果要暴露對象,或是return 物件c,則模組c的exports = c。或直接是module.export = c;總之最後會回傳module c.export = c;所以var c = module c.export = c,模組b中,就可以使用變數c呼叫模組c中的c物件的方法和屬性。
以此類推,最終a模組也能呼叫b模組中b物件的屬性與方法。
不管什麼模組,只要使用了module.export = xx模組,其他模組就可以使用require("xx模組"),呼叫xx模組中的各種方法了。
最終模組的狀態會變成module.status = STATUS.COMPILED。

複製程式碼 程式碼如下:

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) } 
  })
   }

這時args = [module a.export, module jquery.export];

複製程式碼 程式碼如下:

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

這時function中的a和$就是module a.export和module jquery.export。

因為自己現在在研究jquery源碼和jquery框架設計,因此共享一些經驗:
jquery源碼,我在網路上看了很多解析,看著看著就看不下去了。意義不大,推薦妙味課堂的jquery源碼解析。

司徒正美的javascript框架設計,個人覺得難度高,但是精讀後,你就是高級前端工程師了。

玉伯的sea.js,我建議去學習,去用,畢竟是中國人自己做的。我們公司新的專案或重構,都會使用seajs來做。

接下來就是模組化handbars以及mvc的backbone或是mvvm的angular的源碼精讀。這裡我希望有人給我建議,看什麼書,看什麼網站,看什麼影片能夠快速的學習。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn