這個星期折騰了一週,中間沒有什麼時間學習,週末又幹了些其它事情,這個時候正好有時間,咱們一塊兒來繼續學習requireJS吧前端
仍是那句話,小釵以爲requireJS自己仍是有點難度的,估計徹底吸取這個月就過去了,等requireJS學習結束後,咱們的學習流程可能就朝兩個方向走node
① 單頁應用框架/UI庫整理數組
② UML文檔相關/重構思想相關(軟性素質)瀏覽器
而後以上的估計估計會持續三、4個月時間,但願學習下來本身能有不同的提升,成爲一個合格的前端,因而咱們繼續今天的內容吧緩存
通過以前的學習,咱們隊requireJS的大概結構以及工做有了必定認識,可是,咱們對於其中一些細節點事實上仍是不太清晰的,好比裏面的隊列相關閉包
requireJS中有幾種隊列,每種隊列是幹神馬的,這些是咱們須要挖掘的,並且也是真正理解requireJS實現原理的難點app
首先,requireJS有兩個隊列:框架
① globalDefQueue / 全局異步
② defQueue / newContext 閉包ide
這個隊列事實上是一個數組,他們具體幹了什麼咱們還不得而知,可是我下意識以爲他比較關鍵......
咱們這裏來簡單的理一理這兩個隊列
這個是全局性的隊列,與之相關的第一個函數爲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中
第二個涉及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處於newContext閉包環境中,按照以前的知識來看,newContext每次也只會執行一次,因此這個defQueue之後會被各個函數共享
操做defQueue的第一個函數爲
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
/** * 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加載以後作的事情
通過以前的學習,面對requireJS咱們大概知道了如下事情
① require是先將依賴項加載結束,而後再執行後面的函數回調
首先第一個就是一個難點,由於require如今是採用script標籤的方式引入各個模塊,因此咱們不能肯定什麼時候加載結束,因此這裏存在一個複雜的判斷以及緩存
② 依賴列表以映射的方式保存對應的模塊,事實上返回的是一個執行後的代碼,返回多是對象多是函數,可能什麼也沒有(不標準)
這個也是一塊比較煩的地方,意味着,每個define模塊都會維護一個閉包,並且多數時候這個閉包是沒法釋放的,因此真正大模塊的單頁應用有可能越用越卡
面對這一問題,通常採用將大項目分頻道的方式,以避免首次加載過多的資源,防止內存佔用過分問題
③ 加載模塊時候會建立script標籤,這裏爲其綁定了onload事件判斷是否加載結束,如果加載結束,會在原來的緩存模塊中找到對應模塊而且爲其賦值,這裏又是一個複雜的過程
require的總體理解之因此難,我以爲就是難在異步加載與循環依賴一塊,異步加載致使程序比較晦澀
因此咱們再次進入程序看看,這一切是如何發生的,這裏先以main.js爲例
通過以前的學習,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)
第二步處理參數而且調用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模塊並不具備模塊,因此其執行邏輯仍是稍有不一樣的,咱們如今將關注點放到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相關流程
這裏以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結束,感受有點小難,等後面技術有所提升後便再學習吧