看着java中各種import加載,在回過頭來看看javascript還在自己造輪子,寫各種XX的模塊加載框架,ECMASCRIPT6不知什么時候能夠普及。不過DT歸DT,該學的還是要學。
一 同步加載模式(SMD)
同步顧名思義就是按順序依次加載執行,比如A模塊要引用B模塊中的某些函數完成事情,那么此時B模塊必須是已經存在頁面內存中的,A調用順利完成執行下面的操作。例子就是A模塊直接調用document對象,因為document早已存在瀏覽器內存中。
同步模塊代碼實現也比較簡單,解析模塊路徑,執行會掉函數,調用模塊
定義一個模塊
var manger = (function(){ var F = F || {}; F.define = function(str, fn){ var parts = str.split('.'), old = parent = this,//old保存祖父模塊,parent保存當前模塊的父模塊 i = len = 0; for(len = parts.length; i < len; i++){ if(typeof parent[parts[i]] === 'undefined'){ parent[parts[i]] = {}; } old = parent; parent = parent[parts[i]]; } if(fn){ old[parts[--i]] = fn(); } return F; } })();
上面代碼中,define和module分別是模塊的定義和調用,模塊被定義在閉包F上,通過getF()調用,這里用了old和parent2個對象是因為要保存當前模塊的父級模塊和祖父模塊緩存,為了在依次添加完模塊后,可以執行回調函數到當前模塊上。
這里的回調函數也就是一個js文件中要寫的關於模塊的構建,比如下面代碼,包含了構造函數,以及靜態方法;
manger.getF().define('dom', function(){ var dom = function(id){ return document.getElementById(id); } dom.html = function(html){ return this.innerHTML; } return dom; });
調用一個模塊
F.module = function(mod,callback){ var args = [].slice.call(arguments), fn = args.pop(), parts = args[0] && args[0] instanceof Array?args[0]:args, modules = [], modIds = '', i = 0, ilen = parts.length, parent, j, jlen; while(i < ilen){ if(typeof parts[i] === 'string'){ parent = this;//this是模塊緩存器
modIds = parts[i].replace('/^F\./', '').split('.');//modIds=[]
for(j = 0, jlen = modIds.length; j < jlen; j++){ parent = parent[modIds[j]] || false; } modules.push(parent);//將模塊對象裝入modules中,在回調函數中應用
}else{ modules.push(parts[i]); } i++; } fn.apply(null, modules); }
通過將模塊對象引入到回調函數中來執行模塊的調用,關鍵是解析模塊路徑,把需要加載的模塊都放入到modules數組中實現。例子如下
manger.getF().module(['dom'],function(dom){ dom('div1'); });
二 異步加載模式
前言
從上面的同步加載模式來看,模塊開始就被加載到了內存中,沒有異步在“運行時”動態綁定script標簽來加載模塊,那么問題來了如何引用別人沒有寫完的js模塊文件,這就產生了一個異步加載問題,當前各大模塊加載框架都是這么干的
集定義和調用於一身的模塊加載器
F.module = function(url, modDeps, modCallback){ var args = [].slice.call(arguments); var callback = args.pop(); var deps = (args.length && (args[args.length - 1] instanceof Array)) ? args .pop() : []; var url = args.length ? args.pop() : null, params = [], // 依賴模塊序列,這個參數比較重要,它存放了模塊依賴對象 depsCount = 0, // 依賴模塊計數器用來等待加載依賴模塊 i = 0;// 依賴模塊下標 var len; if (len = deps.length) { while(i<len){//依次加載依賴模塊 (function(i){//這個函數在定義的時候已經被執行了,所以首次加載一個模塊會執行這個函數,而不是setModule depsCount++; loadModule(deps[i],function(mod){ params[i] = mod;//構造函數創建的模塊對象傳遞給params【i】 depsCount--; if(depsCount===0){//等待所有的依賴模塊都加載到內存中,才一次性修改該模塊的屬性 setModule(url,params,callback); } }); })(i); /*depsCount++; loadModule(deps[i],function(mod){ params[i] = mod;//構造函數創建的模塊對象傳遞給params【i】 depsCount--; if(depsCount===0){//等待所有的依賴模塊都加載到內存中,才一次性修改該模塊的屬性 setModule(url,params,callback); } });*/ i++; } }else{//這是定義一個沒有依賴的模塊,直接執行回調函數 setModule(url,[],callback);//setModule('lib/event',[],fn) } }
上面代碼中關鍵的一點就是用到了閉包來保存i,如果不使用閉包,i的值只會是最后依賴模塊的數量,而不是每次的結果。關於閉包問題可以看我的博客。
代碼中用到了2個重要的函數就是loadModule和setModule,下面對這2個代碼一些解釋
loadModule = function(name, callback){ var module; if (moduleCache[name]) {//模塊已經在內存中 _module = moduleCache[name]; if (_module.status = 'loaded') {//模塊加載完成 //如果模塊已經加載到頁面中,立即執行模塊的構造函數,並且將構造函數創建的模塊對象傳遞給params[i] setTimeout(callback(_module.exports), 0); } else {//模塊加載完,但是還未執行其中的回調函數,此時只是將回調函數放入到onload數組中去,沒有執行回調 _module.onload.push(callback); } } else {//首次加載該模塊,設置模塊的狀態等等,onload只是一個含有一個元素的數組,存放了回調函數的引用,但是沒有執行回調函數 moduleCache[name] = { name : name, status : 'loading', exports : null, onload : [ callback ] }; loadScript(getUrl(name));//將js文件加載到內存中 } for ( var i in moduleCache) { console.log(moduleCache[i]); } };
loadModule函數加載模塊的時候分為3種情況分別是,在代碼注釋中也寫了,關鍵的一點就是,在js模塊中,如果js文件的狀態是“loaded”那么立即執行callback回調函數,這里使用了settimeout延遲為0毫秒的作用不一定是立即執行,而是等到下一個trick才執行,在前端可以理解為立即執行,並且callback函數給他傳遞了這個模塊的對外接口,這里指的是創建模塊時候函數return的對象。
// 修正js文件,並且執行js的回調函數,params是回調函數的參數,也就是所有依賴模塊對象 setModule = function(name, params, callback){ var _module, fn; if (moduleCache[name]) {//模塊(要執行的模塊)緩存中已經存在了該模塊,當前頁面中已經加載了js文件 _module = moduleCache[name]; _module.status = 'loaded'; _module.exports = callback ? callback.apply(_module, params) : null;//exports中存放着該模塊的方法 while (fn = _module.onload.shift()) { console.log('模塊回調函數:'+fn); console.log('模塊接口:'+_module.exports); for(var i in _module.exports){ console.log(i); } fn(_module.exports); } } else {//匿名模塊,直接執行回調函數,也就是模塊的調用 console.log('匿名模塊:'+callback); for(var i in params){ console.log('參數'+i+':'+params[i]); if(typeof params[i]==='object'){ for(var j in params[i]){ console.log(j+':'+params[i][j]); } } } callback && callback.apply(null, params); } },
從F.module可以看到,只有在依賴模塊都加載到內存中才使用setModule來修正模塊,該函數的作用是將模塊屬性(moduleName,status,exports,onload)都修正為加載完成后的狀態,並且執行回調函數。這里執行的回調函數和loadModule執行的回調函數不同,loadModule是要執行依賴模塊的會掉,而這里是執行最終模塊的回調。
這樣,一個完整的模塊加載器就實現了,並且上面的異步加載器類似於RequireJS ,因為在有關於依賴的時候是提前定義的,並且是一旦定義就必須加載,不符合就近聲明原則,所以有待優化的地方,還望批評指正。