requirejs源碼分析,使用注意要點

本文將深度剖析require.js代碼,爲了是你們更高效、正確的去使用它,本文不會介紹require的基本使用!javascript

概要


先來一個流程圖來概要一下大概流程html

在require中,根據AMD(Asynchronous Module Definition)的思想,即異步模塊加載機制,其思想就是把代碼分爲一個一個的模塊來分塊加載,這樣無疑能夠提升代碼的重用。java

在整個require中,主要的方法就兩個:require和define,咱們先來聊聊requirenode

require


require做爲主函數來引入咱們的「模塊」,require會從自身的的存儲中去查找對應的defined模塊,若是沒有找到,則這時這個模塊有能夠存在三種狀態:loading, enabling, defining,這裏有可能就疑惑了,爲何還會有這麼多狀態呢?正則表達式

這就是require中要注意的地方,若是模塊尚未被加載,那麼它的這三種狀態出現的時機是:數組

  1. loading
    文件尚未加載完畢
  2. enabling
    對該模塊的依賴進行加載和模塊化
  3. defining
    對正在處理的模塊進行加載,並運行模塊中的callback

有同窗就問了,爲何會出現這麼多的狀態呢?js不是單線程操做嗎?拿過來直接加載不就完了嗎?哪來這麼多事呢。而這就是require的神奇之處,咱們先來瞅一瞅require的load方法的主要代碼:app

req.load = function (context, moduleName, url) {
    var config = (context && context.config) || {},
    node;
    if (isBrowser) {
        //create a async script element
        node = req.createNode(config, moduleName, url);

        //add Events [onreadystatechange,load,error]
        .....

        //set url for loading
        node.src = url;

        //insert script element to head and start load
        currentlyAddingScript = node;
        if (baseElement) {
            head.insertBefore(node, baseElement);
        } else {
            head.appendChild(node);
        }
        currentlyAddingScript = null;

        return node;
    } else if (isWebWorker) {
        .........
    }
};

req.createNode = function (config, moduleName, url) {
    var node = config.xhtml ?
        document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
        document.createElement('script');
    node.type = config.scriptType || 'text/javascript';
    node.charset = 'utf-8';
    node.async = true;
    return node;
};

rquire使用的是script標籤去拿js,細心的同窗會注意到node上設定了async屬性(異步加載script標籤),而且在標籤上綁定了load等事件,當文件loading完成後,則要作的主要工做是執行completeLoad事件函數,可是要注意的是這時候把script加載完成後,當即執行的是script標籤內部的內容,執行完後才觸發的completeLoad事件,而在咱們的模塊裏面,必定要用define函數來對模塊進行定義,因此這裏咱們先穿插着來說講define幹了什麼異步

define亂入


define顧名思義是去定義一個模塊,它只是單純的去定義嗎?錯,我不會告訴你define作了你想象不到的最神奇的事情,來瞅瞅define的代碼async

define = function (name, deps, callback) {
    var node,
    context;

    //do for multiple constructor
    ......

    //If no name, and callback is a function, then figure out if it a
    //CommonJS thing with dependencies.
    if (!deps && isFunction(callback)) {
        deps = [];
        //Remove comments from the callback string,
        //look for require calls, and pull them into the dependencies,
        //but only if there are function args.
        if (callback.length) {
            callback
            .toString()
            .replace(commentRegExp, '')
            .replace(cjsRequireRegExp, function (match, dep) {
                deps.push(dep);
            });

            deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
        }
    }

    //If in IE 6-8 and hit an anonymous define() call, do the interactive work.
    if (useInteractive) {
        node = currentlyAddingScript || getInteractiveScript();
        if (node) {
            if (!name) {
                name = node.getAttribute('data-requiremodule');
            }
            context = contexts[node.getAttribute('data-requirecontext')];
        }
    }

    //add to queue line
    if (context) {
        context.defQueue.push([name, deps, callback]);
        context.defQueueMap[name] = true;
    } else {
        globalDefQueue.push([name, deps, callback]);
    }
};

這就是define函數,代碼不是不少,可是新奇的東西倒是有一個!!!那就是代碼中對callback.toString()文原本進行正則匹配,哇,這是什麼鬼呢?咱們看看這兩個replace中的正則表達式是什麼樣的模塊化

commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg;
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g;

第一個正則是用來支掉callback中的註釋的,而第二個正則是用來匹配callback.toString()文本中的require(.....),並將.....這個字段push到queue中,這個方法是否是很變態?如今讓咱們來接着回到require的completeLoad函數

require迴歸


rquire的compeleteLoad函數又作了什麼呢?二話不說,扔一段代碼來看看:

completeLoad : function (moduleName) {
    var found,
    args,
    mod,
    shim = getOwn(config.shim, moduleName) || {},
    shExports = shim.exports;
    
    takeGlobalQueue();
    while (defQueue.length) {
        args = defQueue.shift();
        if (args[0] === null) {
            args[0] = moduleName;
            //If already found an anonymous module and bound it
            //to this name, then this is some other anon module
            //waiting for its completeLoad to fire.
            if (found) {
                break;
            }
            found = true;
        } else if (args[0] === moduleName) {
            //Found matching define call for this script!
            found = true;
        }

        callGetModule(args);
    }
    context.defQueueMap = {};

    //Do this after the cycle of callGetModule in case the result
    //of those calls/init calls changes the registry.
    mod = getOwn(registry, moduleName);

    if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
        if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
            if (hasPathFallback(moduleName)) {
                return;
            } else {
                return onError(makeError('nodefine',
                        'No define call for ' + moduleName,
                        null,
                        [moduleName]));
            }
        } else {
            //A script that does not call define(), so just simulate
            //the call for it.
            callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
        }
    }

    checkLoaded();
}

這個函數主要是去作了從queue中拿出來define裏push進去的字符串,並調用callGetModule去調用模塊,callGetModule又去作了什麼

function callGetModule(args) {
    //Skip modules already defined.
    if (!hasProp(defined, args[0])) {
        getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
    }
}

在require內部,有一個defined全局變量來儲存已經定義好的模塊,若是這個模塊目前沒有定義,那就再作下面的makeModuleMap,這個方法則是用來實現對當前module信息的組裝,並生成一個Map,它將會返回如下的值:

return {
    prefix : prefix,
    name : normalizedName,
    parentMap : parentModuleMap,
    unnormalized : !!suffix,
    url : url,
    originalName : originalName,
    isDefine : isDefine,
    id : (prefix ?
        prefix + '!' + normalizedName :
        normalizedName) + suffix
};

而後再去調用getModule,這也是require裏面來組裝module的主要方法,在require內部定義了Module類,而這個方法則會爲當前的ModuleMap,其中包含了這個模塊的路徑等信息。這裏要注意的是getModule方法裏面擁有一個基於全局context的registry變量,這裏則是用來保存根據ModuleMap來實例化的Module,並將其保存在了registry變量中(當即保存的Module只是一個空殼,後面實例中介紹),後面會介紹代碼的重用如何實現的。

咱們直接來看看Module類是長什麼樣的:

Module = function (map) {
    this.events = getOwn(undefEvents, map.id) || {};
    this.map = map;
    this.shim = getOwn(config.shim, map.id);
    this.depExports = [];
    this.depMaps = [];
    this.depMatched = [];
    this.pluginMaps = {};
    this.depCount = 0;
};

Module.prototype = {
    //init Module
    init : function (depMaps, factory, errback, options) {},

    //define dependencies
    defineDep : function (i, depExports) {},

    //call require for plugins
    fetch : function () {},

    //use script to load js
    load : function () {},

    //Checks if the module is ready to define itself, and if so, define it.
    check : function () {},

    //call Plugins if them exist and defines them
    callPlugin : function () {},

    //enable dependencies and call defineDep
    enable : function () {},

    //register event
    on : function (name, cb) {},

    //trigger event
    emit : function (name, evt) {}
}

new一個Module後,使用init來對Module對象進行初始化,並主要傳入其的依賴數組和工廠化方法。這這麼多的方法裏,主要的兩個方法則是enablecheck方法,這兩個方法是對方法,當init裏調用enable後,下來將要進行的就是一個不斷重複的過程,可是過程的主角在一直改變。

遞歸


上面說的這個過程那就是在初始化Model的時候去查找它的依賴,再去用load方法異步地去請求依賴,而依賴又是一個個Module,又會再對本身自身的依賴的依賴進行查找。因爲這個過程都是異步進行的,因此都是經過事件監聽回調來完成調用的,咱們來舉下面的例子:

  • A 的依賴有 B C
  • B 的依賴有 C D
  • C 的依賴有 A B

這是一個很繞的例子,如A,B,C都有本身的方法,而咱們在實現時都互相調用了各自的方法,咱們姑且不討論這種狀況的現實性。

當若是我去require("A")時,require去查找defined中是否有A模塊,若是沒有,則去調用makeModuleMap來爲即將調用的模塊實例一個ModuleMap並加入到defined中,再用ModuleMap實例化一個Module加入到registry中,可是這時候的Module是一個空殼,它是隻存儲了一些模塊相關的依賴等,模塊裏的exports或者callback是尚未被嵌進來,由於這個文件根本沒有被加載呀!

註冊時觸發Module.init方法去異步加載文件(使用script)。加載完畢後,觸發A裏的define函數,define函數經過參數或callback裏查找A模塊須要的依賴,即B和C模塊,將B,C加入到A的依賴數組中。這時則觸發completeLoad函數,這時complete再去從queue中遍歷,調用callGetModule去查找B、C模塊,這時則會建立B和C模塊的ModuleMap,根據ModuleMap去實例化空殼Module,(調用異步load加載,再觸發define等,繼續查找依賴…………),再接下來會作checkLoaded,咱們看看這個函數:

function checkLoaded() {
    var err,
    usingPathFallback,
    waitInterval = config.waitSeconds * 1000,
    //It is possible to disable the wait interval by using waitSeconds of 0.
    expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
    noLoads = [],
    reqCalls = [],
    stillLoading = false,
    needCycleCheck = true;

    //Do not bother if this call was a result of a cycle break.
    if (inCheckLoaded) {
        return;
    }

    inCheckLoaded = true;

    //Figure out the state of all the modules.
    eachProp(enabledRegistry, function (mod) {
        var map = mod.map,
        modId = map.id;

        //Skip things that are not enabled or in error state.
        if (!mod.enabled) {
            return;
        }

        if (!map.isDefine) {
            reqCalls.push(mod);
        }

        if (!mod.error) {
            //If the module should be executed, and it has not
            //been inited and time is up, remember it.
            if (!mod.inited && expired) {
                if (hasPathFallback(modId)) {
                    usingPathFallback = true;
                    stillLoading = true;
                } else {
                    noLoads.push(modId);
                    removeScript(modId);
                }
            } else if (!mod.inited && mod.fetched && map.isDefine) {
                stillLoading = true;
                if (!map.prefix) {
                    //No reason to keep looking for unfinished
                    //loading. If the only stillLoading is a
                    //plugin resource though, keep going,
                    //because it may be that a plugin resource
                    //is waiting on a non-plugin cycle.
                    return (needCycleCheck = false);
                }
            }
        }
    });

    if (expired && noLoads.length) {
        //If wait time expired, throw error of unloaded modules.
        err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
        err.contextName = context.contextName;
        return onError(err);
    }

    //Not expired, check for a cycle.
    if (needCycleCheck) {
        each(reqCalls, function (mod) {
            breakCycle(mod, {}, {});
        });
    }

    if ((!expired || usingPathFallback) && stillLoading) {
        //Something is still waiting to load. Wait for it, but only
        //if a timeout is not already in effect.
        if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
            checkLoadedTimeoutId = setTimeout(function () {
                    checkLoadedTimeoutId = 0;
                    checkLoaded();
                }, 50);
        }
    }

    inCheckLoaded = false;
}

這個函數對全部已在registry中的Module進行遍歷,並來判斷其是否已經完成了定義(定義是在Module.check函數裏完成的,定義完成後ModuleMap.isDefined = true,並將其從registry中刪除,其會將真正的模塊內容注入到對應的defined中),注意這裏有一個重要的地方,checkoutLoadTimeoutId是一個間隔爲50ms的setTimeout函數,即當在加載的時候會不斷輪詢去查看全部模塊是否已經加載好了,由於全部的模塊都是異步進行加載的,因此這樣能夠徹底保證全部模塊進行徹底加載,並進行了過時設定。

接着上面的例子講,當加載B模塊時,會去查找A和C模塊,這時候A模塊是已經加載的,可是不能肯定C是否已經加載好,可是這時的C模塊空殼Module已經加入到了registry中,因此這時會像上面去輪詢C模塊是否加載,C模塊不加載好,是沒法對B模塊進行注入的,B模塊在這一階段還是那一個registry裏的空殼Module,直至C模塊已經定義,B模塊的depCount成爲0,才能夠繼續運行去注入本身。在對模塊進行define的時候,用上了defining,是爲了防止內部的factory進行加工時,再去嘗試去define這個Module,就像一個圈同樣,掐斷了它。

這就是整個require工做的流程,其中主要使用了異步加載,因此讓這個思想變得異常的複雜,可是帶來的倒是性能上的優化,須要咱們注意的是:

在使用require時,咱們須要注意依賴包的引入,若是咱們把B的改爲define("B",[],callback),這時B是沒有callback依賴預讀,那麼咱們在引入A模塊的時候異步加載了B和C模塊,可是B模塊裏使用了C模塊的方法,這裏的B是直接運行的,並不去檢測其的依賴包是否加載完畢,因此這時的B運行時碰到require("C")時,C模塊是否加載好是不肯定的,這時候代碼會不會出問題就是網速的問題了……………………

當心


咱們在使用時要當心define()的用法:

  • define(name, dependencies, callback)
    將依賴寫在參數dependencies中,這樣require時會對裏面的依賴進行加載,加載完後纔會執行callback
  • define(dependencies, callback)
    同上
  • define(name, callback)
    直接在callback中require依賴,會對callback.toString()進行正則查找require(....),一樣加載查找出的全部依賴並加載完後執行callback
  • define(callback)
    同上

使用時千萬不能在第1、二種狀況下直接require依賴,這樣並不能保障該模塊是否已被定義下執行了callback

在使用後兩種狀況時,必需要注意的一點是,在require中對callback使用了callback.length判斷,若是回調中沒有加參數,則不會進行callback.toString()進行查找,因此咱們在這兩種狀況使用時務必要加上參數,哪怕是沒用的,如define(function(require){})

能夠看出,require老是在自動的分析對模塊的依賴並去加載它們,若是咱們一次性在文件中都互相引入了全部模塊,那就會使得整個模塊化過程變得累贅不堪,就是說即便當前只須要兩三個模塊來完成頁面,而其它很十幾二十個模塊都會被加載進來,雖然模塊化達成了,可是效率倒是降低了。因此在使用的使用咱們要去智能地判斷目前當即須要的模塊,而非當即的模塊咱們能夠在空閒時間進行加載,而並是不去影響主頁面的加載和渲染這在實現單頁面應用時及其重要

Finish.

相關文章
相關標籤/搜索