270行程式碼實現一個AMD模組載入器

fiveoneLei發表於2018-03-19

AMD模組載入器

模組載入對於前端來說是非常重要的一個知識點.當前的主流模組載入方式有AMDCMDCommonJsnode環境下)。AMDCMD有很多共同之處例如都是檔案提前並行載入,依賴載入完成通知模組,模組監聽依賴是否全部執行完畢,如果直接執行factory.核心原理區別其實並不大,只不過程式碼的執行順序有所區別而已。AMD提前執行CMD按需執行.而CMDCommonJs的區別在於檔案的載入順序不同,CommonJS檔案是就近載入,程式碼就近執行.這裡我用最少的程式碼來實現一個AMD模組載入器以便大家來真正理解掌握前端的模組載入

fw.js

不廢話,直接上程式碼。重要的程式碼塊我已寫上註釋

var fwjs, require, define;
(function (global) {
    var req, 
        ob = Object.prototype,
        toString = ob.toString,
        hasOwn = ob.hasOwnProperty,
        version = "1.0.0",
        contexts = {},
        head = document.getElementsByTagName('head')[0],
        globalDefQueue = [],
        defContextName ="_";

    function isFunction(f) {
        return toString.call(f) == "[object Function]"
    }
    function isArray(arr) {
        return toString.call(arr) == "[object Array]"
    }
    function getOwn(obj, prop) {
        return hasOwn.call(obj, prop) && obj[prop];
    }
    // 建立script節點
    function createScriptNode(){
        var node =  document.createElement('script');
        node.type = 'text/javascript';
        node.charset = 'utf-8';
        node.async = true;
        return node;
    };
    // 載入js檔案
    function loadScript(fn, moduleName, url){
        var node = createScriptNode();
        node.setAttribute('data-fwmodule', moduleName);
        node.addEventListener('load', fn, false);
        node.src = url;
        head.appendChild(node)
    }
    function createContext(){
        var context = {},
            registry = {},
            undefEvents = {},
            defined = {},
            urlLoaded = {},
            defQueue=[],
            requireCounter = 1
        ;
        function makeModuleMap(name, parentModuleMap){
            var isDefine = true,
                normalizedName = "",
                originalName = name,
                url = name,
                parentName = parentModuleMap ? parentModuleMap.name : "";
            if (!name) {
                    isDefine = false;
                    name = 'fw' + (requireCounter += 1);
            }
            // 在這裡並沒有對id和name進行處理 主要是不支援config
            return {
                name: name,
                parentMap: parentModuleMap,
                url: name,
                originalName: originalName,
                isDefine: isDefine,
                id: name
            };
        }
        function getModule(depMap) {
            var id = depMap.id,
                mod = getOwn(registry, id);

            if (!mod) {
                mod = registry[id] = new context.Module(depMap);
            }

            return mod;
        }
        // 構建Module類
        function Module(map){
            this.events = getOwn(undefEvents, map.id) || {};
            this.map = map;
            this.depExports = [];
            this.depMaps = [];
            this.depMatched = [];  // 依賴是否已defined
            this.depCount = 0;
        }
        Module.prototype = {
            // 模組初始化
            init: function(depMaps, factory){
                if (this.inited) {
                    return;
                }
                this.factory = factory;
                this.inited = true;
                this.depMaps = depMaps || [];
                this.enable();
            },
            // 啟用模組
            enable:function(){
                this.enabled = true;
                this.enabling = true;
                this.depMaps.forEach(function(depMap, i) {
                    var mode = null;
                    if (typeof depMap == "string") {
                        depMap = makeModuleMap(depMap, this.map.isDefine ? this.map : null);
                        mod = getOwn(registry, depMap.id);
                        
                        this.depCount += 1;
                        this.depMaps[i] = depMap;
                        var fn = function (depExports) {
                            if (!this.depMatched[i]) {
                                this.depMatched[i] = true;
                                this.depCount -= 1;
                                this.depExports[i] = depExports;
                            }
                            this.check();
                        }.bind(this)
                        // 如果模組已經載入過
                        if (getOwn(defined, depMap.id) && mod.defineEmitComplete) {
                            fn(defined[depMap.id]);
                        } else {
                            mod = getModule(depMap);
                            // 繫結defined事件,監聽依賴的載入,每一個依賴載入完成,模組都會收集依賴的exports,但所有的依賴載入完畢模組才會執行 
                            mod.on("defined", fn)  
                        }
                        mod = registry[depMap.id];
                        if (mod && !mod.enabled) {
                            mod.enable()
                        }
                    }
                }.bind(this))
                this.enabling = false;
                this.check();
            },
            // 執行模組
            check:function(){
                if (!this.enabled || this.enabling) {
                    return;
                }
                var id = this.map.id,
                    depExports = this.depExports,
                    exports = this.exports,
                    factory = this.factory;
                if (!this.inited) {
                    this.load(); //  
                } else if (!this.defining){
                    this.defining = true;                               // defining下面程式碼每個模組只執行一次
                    if (isFunction(factory)) {                          // 模組的factory只允許是函式
                                                                        // 只有模組的依賴全部執行完,才會執行factory
                        if (this.depCount < 1 && !this.defined) {       // 只有暴露出exports defined屬性才為true
                            exports = factory.apply(this, depExports)
                            this.exports = exports;
                            if (this.map.isDefine) {
                                defined[id] = exports;
                            }
                            this.defined = true; 
                        }  
                    }       
                    this.defining = false;
                    if (this.defined && !this.defineEmitted) {
                        this.defineEmitted = true;
                         
                        this.emit('defined', this.exports);
                        this.defineEmitComplete = true;
                    }
                }    
            },
            load(){
                if (this.loaded) {
                    return;
                }
                this.loaded = true;
                var url = this.map.url;

                //Regular dependency.
                if (!urlLoaded[url]) {       
                    urlLoaded[url] = true;
                    loadScript(context.onScriptLoad, this.map.id, url) 
                }
            },
            on: function (name, cb) {
                var cbs = this.events[name];
                if (!cbs) {
                    cbs = this.events[name] = [];
                }
                cbs.push(cb);
            },
            emit: function(name, data){
                var evts = this.events[name] || [];
                evts.forEach(function(cb){
                    cb(data);
                })
            }
        }
        // 將globalQueue轉入defQueue
        function getGlobalQueue() {
            //Push all the globalDefQueue items into the context's defQueue
            if (globalDefQueue.length) {
                globalDefQueue.forEach(function(queueItem) {
                    var id = queueItem[0];
                    if (typeof id === 'string') {
                        context.defQueueMap[id] = true;
                    }
                    defQueue.push(queueItem);
                });
                globalDefQueue = [];
            }
        }
        context.Module = Module;
        context.require = function (deps, callback){
            //console.log(deps, callback)
            var requireMod = getModule(makeModuleMap(null));
            requireMod.init(deps, callback);
        };
        context.onScriptLoad = function(evt){
            if (evt.type == "load") {
                var node = evt.currentTarget || evt.srcElement;
                node.removeEventListener('load', context.onScriptLoad, false);
                var id = node.getAttribute('data-fwmodule')
                context.completeLoad(id);
            }
        };
        context.completeLoad = function(moduleName){
            var found, args;
            // 提取當前載入的模組
            getGlobalQueue();
            while (defQueue.length) {
                args = defQueue.shift();
                if (args[0] === null) {
                    args[0] = moduleName;
                    if (found) {
                        break;
                    }
                    found = true;
                } else if (args[0] === moduleName) {
                    found = true;
                }
                if (!getOwn(defined, args[0])) {
                    // 依賴載入完成之後,對檔案進行初始化
                    getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
                }
            }
        }
        return context;
    }
    // 入口的require函式
    req = fwjs = require = function(deps, callback) {
        var context = {},
            contextName = defContextName;
        if (isFunction(deps)) {
            deps = [];
            callback = deps
        }
        context = getOwn(contexts, contextName);
        if (!context){
            context = contexts[contextName] = createContext();
        }
        return context.require(deps, callback)
    }
    // define只允許匿名模組
    define = function(deps, callback){
        if (!isArray(deps)) {
            callback = deps;
            deps = [];
        }
        var name = null, context;
        globalDefQueue.push([name, deps, callback]);
        globalDefQueue[name] = true;
    }

}(this))
複製程式碼

程式碼為什麼會這麼少?

  1. 不相容IE
  2. 不支援config(不需要處理大量的複雜的pluigin, shim, baseUrl之類的全域性配置)
  3. 只支援匿名模組(不需要處理name),模組只支援用函式包裹
  4. 僅供學習使用,以理解AMD的模組載入原理

實現原理

因為程式碼很少,大家看幾遍就很容易看懂了,所以這個我只是簡單的說下吧

每一個依賴載入執行完都讓depCount減一併且模組會check,如果depCount為0就會執行factoryfactory執行完繼續向上通知因為模組本身可以是依賴, 直至入口的require模組執行

demo

index.js

require( ["./src/app.js", "./src/app1.js", "./src/m/app2.js"],
    function(a, b, c) {
        console.log(a, b, c)
    }
);
複製程式碼

app.js

define(function () {
    //Do setup work here
    return {
        name:"app"
    }
});
複製程式碼

app1.js

define(function () {
    //Do setup work here
    return {
        name: "app1"
    }
});
複製程式碼

m/app2.js

define(["./src/app.js"],function (a) {
    console.log(a)
    //Do setup work here
    return {
        name:"app2"
    }
});
複製程式碼

展示

270行程式碼實現一個AMD模組載入器

RequireJs

我的程式碼是參照RequireJs編寫的這裡是官網文件 用興趣的可以直接閱讀RequireJs的原始碼

結束

這是github地址,如果大家覺得寫的好可以給我個star

相關文章