前言
這個星期折騰了一周,中間沒有什么時間學習,周末又干了些其它事情,這個時候正好有時間,我們一起來繼續學習requireJS吧
還是那句話,小釵覺得requireJS本身還是有點難度的,估計完全吸收這個月就過去了,等requireJS學習結束后,我們的學習流程可能就朝兩個方向走
① 單頁應用框架/UI庫整理
② UML文檔相關/重構思想相關(軟性素質)
然后以上的估計估計會持續3、4個月時間,希望學習下來自己能有不一樣的提高,成為一個合格的前端,於是我們繼續今天的內容吧
requireJS中的隊列
經過之前的學習,我們隊requireJS的大概結構以及工作有了一定認識,但是,我們對於其中一些細節點事實上還是不太清晰的,比如里面的隊列相關
requireJS中有幾種隊列,每種隊列是干神馬的,這些是我們需要挖掘的,而且也是真正理解requireJS實現原理的難點
首先,requireJS有兩個隊列:
① globalDefQueue / 全局
② defQueue / newContext 閉包
這個隊列事實上是一個數組,他們具體干了什么我們還不得而知,但是我下意識覺得他比較關鍵......
我們這里來簡單的理一理這兩個隊列
globalDefQueue
這個是全局性的隊列,與之相關的第一個函數為takeGlobalQueue
takeGlobalQueue
/** * Internal method to transfer globalQueue items to this context's * defQueue. */ function takeGlobalQueue() { //Push all the globalDefQueue items into the context's defQueue if (globalDefQueue.length) { //Array splice in the values since the context code has a //local var ref to defQueue, so cannot just reassign the one //on context. apsp.apply(defQueue, [defQueue.length - 1, 0].concat(globalDefQueue)); globalDefQueue = []; } }
這個函數中涉及到了defQueue中的的操作,每一次有效操作后都會將全局隊列清空,其中有一個apsp方法這個是數組的splice方法
該函數主要用於將globalDefQueue中的數據導入defQueue,而globalDefQueue只會有可能在define函數出被壓入數據,具體原因還得往后看
所以這里的takeGlobalQueue其實就如注釋所說,將全局隊列中的項目轉入context defQueue中
define
第二個涉及globalDefQueue函數為define

/** * The function that handles definitions of modules. Differs from * require() in that a string for the module should be the first argument, * and the function to execute after dependencies are loaded should * return a value to define the module corresponding to the first argument's * name. */ define = function (name, deps, callback) { var node, context; //Allow for anonymous modules if (typeof name !== 'string') { //Adjust args appropriately callback = deps; deps = name; name = null; } //This module may not have dependencies if (!isArray(deps)) { callback = deps; deps = null; } //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); }); //May be a CommonJS thing even without require calls, but still //could use exports, and module. Avoid doing exports and module //work though if it just needs require. //REQUIRES the function to expect the CommonJS variables in the //order listed below. 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')]; } } //Always save off evaluating the def call until the script onload handler. //This allows multiple modules to be in a file without prematurely //tracing dependencies, and allows for anonymous module support, //where the module name is not known until the script onload event //occurs. If no context, use the global queue, and get it processed //in the onscript load callback. (context ? context.defQueue : globalDefQueue).push([name, deps, callback]); };
他會根據context是否初始化決定當前鍵值標識存於哪個隊列,據代碼看來,如果是標准瀏覽器應該都會先走globalDefQueue隊列
然后就沒有然后了,我們接下來再看看吧
defQueue
首先defQueue處於newContext閉包環境中,按照之前的知識來看,newContext每次也只會執行一次,所以這個defQueue以后會被各個函數共享
操作defQueue的第一個函數為
intakeDefines
function intakeDefines() { var args; //Any defined modules in the global queue, intake them now. takeGlobalQueue(); //Make sure any remaining defQueue items get properly processed. while (defQueue.length) { args = defQueue.shift(); if (args[0] === null) { return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1])); } else { //args are id, deps, factory. Should be normalized by the //define() function. callGetModule(args); } } }
引入定義,第一件事情就是將globalDefQueue中的項目移入defQueue中,而后將其中的項目一個個取出並執行callGetModule方法,但是我這里好像都沒有效果,這塊先忽略之
第二個函數為completeLoad
completeLoad

/** * Internal method used by environment adapters to complete a load event. * A load event could be a script load or just a load pass from a synchronous * load call. * @param {String} moduleName the name of the module to potentially complete. */ 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); } //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(); },
這個會將globalDefQueue中的隊列項搞到defQueue中,然后處理一下就調用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]); } }
這個時候就會由全局registry中獲取當前的模塊了,然后執行他的init方法,這里會加載script標簽,將其依賴項載入,這里還會涉及到registry的操作,我們放到后面來學習
而completeLoad是在script標簽加載結束后調用的方法
/** * callback for script loads, used to check status of loading. * * @param {Event} evt the event from the browser for the script * that was loaded. */ onScriptLoad: function (evt) { //Using currentTarget instead of target for Firefox 2.0's sake. Not //all old browsers will be supported, but this one was easy enough //to support and still makes sense. if (evt.type === 'load' || (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { //Reset interactive script so a script node is not held onto for //to long. interactiveScript = null; //Pull out the name of the module and the context. var data = getScriptData(evt); context.completeLoad(data.id); } },
所以我們這里來重新整理下requireJS的執行流程(可能有誤)
① 引入requireJS標簽后,首先執行一些初始化操作
② 執行req({})初始化newContext,並且保存至contexts對象中
③ 執行req(cfg),將讀取的data-main屬性並且封裝為參數實例化模塊
④ 執行main.js中的邏輯,執行require時候,會一次加載name與say
⑤ 調用依賴時候會根據define進行設置將加載好的標簽引入鍵值對應關系,執行點是load事件
所以關鍵點再次回到了main.js加載之后做的事情
require方法
經過之前的學習,面對requireJS我們大概知道了以下事情
① require是先將依賴項加載結束,然后再執行后面的函數回調
首先第一個就是一個難點,因為require現在是采用script標簽的方式引入各個模塊,所以我們不能確定何時加載結束,所以這里存在一個復雜的判斷以及緩存
② 依賴列表以映射的方式保存對應的模塊,事實上返回的是一個執行后的代碼,返回可能是對象可能是函數,可能什么也沒有(不標准)
這個也是一塊比較煩的地方,意味着,每一個define模塊都會維護一個閉包,而且多數時候這個閉包是無法釋放的,所以真正大模塊的單頁應用有可能越用越卡
面對這一問題,一般采用將大項目分頻道的方式,以免首次加載過多的資源,防止內存占用過度問題
③ 加載模塊時候會創建script標簽,這里為其綁定了onload事件判斷是否加載結束,若是加載結束,會在原來的緩存模塊中找到對應模塊並且為其賦值,這里又是一個復雜的過程
require的整體理解之所以難,我覺得就是難在異步加載與循環依賴一塊,異步加載導致程序比較晦澀
所以我們再次進入程序看看,這一切是如何發生的,這里先以main.js為例
再說main模塊的加載
經過之前的學習,main模塊加載之前會經歷如下步驟
① require調用req({})初始化一個上下文環境(newContext)
② 解析頁面script標簽,碰到具有data-main屬性的標簽便停下,並且解析他形成第一個配置項調用req(cfg)
③ 內部調用統一入口requirejs,並取出上文實例化后的上下文環境(context),執行其require方法
④ 內部調用localRequire(makeRequire)方法,這里干了比較重要的事情實例化模塊
⑤ 模塊的實例化發生在localRequire中,這里的步驟比較關鍵
首先,這里會調用nextTick實際去創建加載各個模塊的操作,但是這里有一個settimeout就比較麻煩了,所有的操作會拋出主干流程之外
這樣做的意義我暫時不能了解,可能這段邏輯會異步加載script標簽,若是不拋到主干流程外會有問題吧,若是您知道請告知
nextTick使用 settimeout 的原因不明,待解決/經測試不加延時可能導致加載順序錯亂
我們這里干一件不合理的事情,將nexttick的延時給去掉試試整個邏輯
req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { setTimeout(fn, 4); } : function (fn) { fn(); }; req.nextTick = function (fn){ fn();};
如此,除了script加載一塊就不再具有異步的問題了,這里我們來從新理一理
深入req(cfg)
第一步調用req(cfg)
第二步處理參數並且調用require
可以看到,經過require.config(config)的處理,相關的參數已經放到了實例里面
第三步調用localRequire,並且進入nextTick流程,一個要注意的地方是這里的this指向的是context
第四步執行intakeDefines,將全局的依賴裝入,這里是沒有的
第五步實例化模塊(makeModuleMap),建立映射關系,最后會返回類似這樣的東西
第六步便將此映射關系傳入getModule建立相關模塊,然后傳入該映射關系對象建立模塊,Module類根據參數對象作簡單初始化便返回
第七步調用mod的init方法真正操作該模塊,這里會執行加載邏輯Module的init方法,最后會到context的load方法加載script標簽,值得注意的是加載結束后這里會綁定onScriptLoad方法
第八步加載成功后會調用context.completeLoad(data.id)方法
因為之前定義過該模塊了,這里只是將其取出(mod = getOwn(registry, moduleName))然后再調用其模塊init方法又會走一連串邏輯,最后再check一塊結束
if (this.map.isDefine && !this.ignore) { defined[id] = exports; if (req.onResourceLoad) { req.onResourceLoad(context, this.map, this.depMaps); } }
因為每一個加載模塊都會定義一個事件,在其實際加載結束后會執行之
if (this.defined && !this.defineEmitted) { this.defineEmitted = true; this.emit('defined', this.exports); this.defineEmitComplete = true; }
最后會調用checkLoaded檢查是否還有未加載的模塊,總之這步結束后基本上就main.js就加載結束了,這個由全局contexts中的defined對象可以看出
這里仍然有一塊比較難,因為在main.js加載結束前還未執行其load事件,其下一步的require流程又開始了
contexts._.defined
Object {}
這個時候全局的defined還沒有東西呢,所以他這里會有一個狀態機做判斷,否則最后不會只是main.js中的fn
require(['name', 'say'], function (name, say) { say(name); });
他們判斷的方式就是不停的check,不停的check,直到加載成功結束
main.js中的require
因為main模塊並不具有模塊,所以其執行邏輯還是稍有不同的,我們現在將關注點放到main.js中的require相關邏輯
require(['name', 'say'], function (name, say) { say(name); });
首次進入這個邏輯時候事實上main.js的onload事件並未執行,所以全局contexts._.defined對象依舊為空,這里進入了實際模塊的加載邏輯既有依賴項又有回調
PS:這里有一個比較有意思的做法就是將原來的nextTick的settimeout干掉這里的情況會有所不同
依舊進入context.require流程
return context.require(deps, callback, errback);
期間會碰到main.js onload事件觸發,並導致
contexts._.defined => Object {main: undefined}
第二步便是這里的會創建一個模塊,這個與,而后調用其init方法,這里需要注意的是傳入了deps(name, say)依賴,所以這里的depMaps便不為空了
並且這里將當前回調傳給factory,並且將依賴的name與say模塊保存
this.factory = factory;
this.depMaps = depMaps && depMaps.slice(0);
進入enable流程,首先注冊當前對象之閉包(newContext)enableRegistry中
這里有一個操作是如果具有依賴關系,我們這里便依賴於say以及name會執行一個邏輯
//Enable each dependency each(this.depMaps, bind(this, function (depMap, i) { var id, mod, handler; if (typeof depMap === 'string') { //Dependency needs to be converted to a depMap //and wired up to this module. depMap = makeModuleMap(depMap, (this.map.isDefine ? this.map : this.map.parentMap), false, !this.skipMap); this.depMaps[i] = depMap; handler = getOwn(handlers, depMap.id); if (handler) { this.depExports[i] = handler(this); return; } this.depCount += 1; on(depMap, 'defined', bind(this, function (depExports) { this.defineDep(i, depExports); this.check(); })); if (this.errback) { on(depMap, 'error', bind(this, this.errback)); } } id = depMap.id; mod = registry[id]; //Skip special modules like 'require', 'exports', 'module' //Also, don't call enable if it is already enabled, //important in circular dependency cases. if (!hasProp(handlers, id) && mod && !mod.enabled) { context.enable(depMap, this); } }));
循環的載入其依賴項,並形成模塊,這里都會搞進enableRegistry中,比如這段邏輯結束前后有所不同
事實上對應模塊初始化已經結束,進入了script待加載邏輯,只不過暫時卡到這里了......
然后這里會進入其check邏輯,由於這里defineDep等於2所以不會執行函數回調,而直接跳出,這里有一個關鍵便是我們的Registry未被清理
以上邏輯只是在main.js中require方法執行后所執行的邏輯,確切的說是這段代碼所執行的邏輯
requireMod.init(deps, callback, errback, { enabled: true });
然后會執行一個checkLoaded方法檢測enabledRegistry中未加載完成的模塊並且進行清理,這段邏輯比較關鍵
function checkLoaded() { var map, modId, 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) { 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 still waiting on loads, and the waiting load is something //other than a plugin resource, or there are still outstanding //scripts, then just try back later. 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; }
他首先會遍歷enableRegistry取出其中定義的模塊,並且將沒有加載成功的模塊標識注入noLoads數組,如果過期了這里就會報錯
如果上述沒問題還會做循環依賴的判斷,主要邏輯在breakCycle中,因為我們這里不存在循環依賴便跳出了,但還未結束
我們這里開始了遞歸檢測依賴是否載入
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); } }
如果模塊沒有載入,這里就會一直繼續,直到所有模塊加載結束,其判斷點又在各個define方法中,define方法會根據鍵值改變對應模塊的標識值
幾個關鍵判斷點為:
① checkLoadedTimeoutId
② inCheckLoaded
③ stillLoading
但是最終的判斷點事實上來源與mod的mod.inited/fetched/isDefine等屬性,所以我們這里需要來理一理
首次模塊執行init方法時會執行
this.inited = true;
因為初始化時候動態的傳入了enabled為true所以首次會執行enable邏輯
//nextTick requireMod.init(deps, callback, errback, { enabled: true }); if (options.enabled || this.enabled) { //Enable this module and dependencies. //Will call this.check() this.enable(); } else { this.check(); }
於是達成enabled為true的條件,這里並且會為該模塊的依賴執行enable操作,並且為其支持defined事件在加載結束后會觸發之
each(this.depMaps, bind(this, function (depMap, i) { var id, mod, handler; if (typeof depMap === 'string') { //Dependency needs to be converted to a depMap //and wired up to this module. depMap = makeModuleMap(depMap, (this.map.isDefine ? this.map : this.map.parentMap), false, !this.skipMap); this.depMaps[i] = depMap; handler = getOwn(handlers, depMap.id); if (handler) { this.depExports[i] = handler(this); return; } this.depCount += 1; on(depMap, 'defined', bind(this, function (depExports) { this.defineDep(i, depExports); this.check(); })); if (this.errback) { on(depMap, 'error', bind(this, this.errback)); } } id = depMap.id; mod = registry[id]; //Skip special modules like 'require', 'exports', 'module' //Also, don't call enable if it is already enabled, //important in circular dependency cases. if (!hasProp(handlers, id) && mod && !mod.enabled) { context.enable(depMap, this); } }));
這里的邏輯比較關鍵,而后才執行該模塊的check方法
PS:讀到這里,才大概對requireJS的邏輯有一定認識了
跳入check方法后便會將defining設置為true因為這里的依賴項未載入結束,所以這里的depCount為2,所以不會觸發
this.defined = true;
所以下面的遞歸settimeout會一直執行,直到成功或者超時,這里我們進入define相關流程
define方法
這里以say為例,在加載文件結束時候會觸發其define方法,這里主要向globalDefQueue中插入當前模塊的隊列,而這里上面做過介紹
而這里的關鍵會在script標簽執行onload事件時候將全局隊列的東西載入context.defQueue
而這個時候又會根據相關的映射id(由event參數獲取),實例化相關模塊(事實上是取得當前模塊,之前已經實例化),這個時候又會進入check邏輯,這個時候是say模塊的check邏輯
say無依賴,並且加載結束,這里會將當前模塊與其返回值做依賴,這里是一個函數,這里factory與exports的關系是
然后會將當前鍵值右Registry相關刪除,完了便會進入下面的邏輯,值得注意的是這里會觸發前面為say模塊注冊的defined事件
PS:這里一定要注意,這里的say模塊里面定義的家伙被執行了!!!
//注冊點 on(depMap, 'defined', bind(this, function (depExports) { this.defineDep(i, depExports); this.check(); })); //觸發點 this.emit('defined', this.exports); //關鍵點,用於消除依賴 this.defineDep(i, depExports);
所以,我們的整體邏輯基本出來了
結語
最后,我們來一次總結,對初次的requireJS學習畫下一個初步的句點
① requireJS會初始化一個默認上下文出來
req({}) => newContext
② 加載main.js,main.js與基本模塊不太一致,加載結束便會執行里面邏輯對主干流程沒有太大影響
③ 執行main.js中的require.config配置,最后調用require方法
④ 調用時候會將數組中的依賴項載入,並且實例化一個匿名模塊出來(mod)
因為主干(匿名)模塊依賴於say與name,所以會在enable中實例化兩個模塊並且將當前實例depCount設置為2
⑤ 各個依賴模塊也會執行加載操作,say以及name,若是有依賴關系會循環執行enable
⑥ 會執行主干模塊的check操作由於depCount為2便執行其它邏輯,這里為其注冊了defined事件
⑦ 執行checkLoaded方法,這里會開始遞歸的檢查模塊是否加載結束,一定要在主干模塊depCount為0 時候才會執行其回調,並且會傳入say與name返回值做參數
⑧ 當模塊加載結束后會觸發其onScriptLoad => completeLoad事件
⑨ 因為各個define模塊會想全局隊列壓入標識的值,並且會根據他獲取相關模塊並且執行其init事件
10 這個時候會執行模塊的實例化init方法,並且會檢測該模塊的依賴,say沒有依賴便繼續向下,將其factory方法執行回指exports(具有參數,參數是依賴項)
PS:其依賴項是在解除依賴時候注入的defineDep
11 最后所有依賴模塊加載時候,最后主干的depCount也就變成了0了,這個時候便會執行類似say的邏輯觸發回調
這里的關鍵就是,加載主干模塊時候會檢查器依賴項,並且為每一個依賴項注冊defined事件,其事件又會執行check方法
這也意味着,每一個依賴模塊檢查成功事實上都有可能執行主干流程的回調,其條件是主干的depCount為0,這塊就是整個requireJS的難點所在......
幾個關鍵便是
① require時候的模塊定義以及為其注冊事件
② 文件加載結束define將該模塊壓入全局隊列
③ script加載成功后觸發全局隊列的檢查
④ 各個子模塊加載結束,並且接觸主模塊依賴執,並且將自我返回值賦予行主模塊實例數組depExports
⑤ 當主模塊depCount為0 時候終於便可以觸發了,於是邏輯結束
最后,小釵渾渾噩噩的初步學習requireJS結束,感覺有點小難,等后面技術有所提高后便再學習吧