js模塊加載詳解


看着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 ,因為在有關於依賴的時候是提前定義的,並且是一旦定義就必須加載,不符合就近聲明原則,所以有待優化的地方,還望批評指正。

 


免責聲明!

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



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