前言
經過昨天的學習,我們大概了解到了requireJS的主要結構,這里先大概的回顧一下
首先從總體結構來說,require這里分為三塊:
① newContext之前變量聲明或者一些工具函數
② newContext大家伙
③ 解析script標簽抽出data-main,並提供幾個對外接口
從流程上講,大概發生了這些事情:
① script標簽引入requireJS后,便會初始化一些變量以及函數,並不干實際的事情
② 在主干結構第三步之前會使用req({})調用(並且只會調用一次)newContext方法由此會維護一個newContext的閉包環境,之后的很多變量全在其中
③ 取出script標簽中的data-main參數,做第一次簡單參數處理(這里有兩個全局變量contexts以及cfg比較重要)
在此調用req(cfg),這里便會進行實際的操作了
所以,關鍵還是第二次調用req(cfg),這里由涉及到很多細節的地方了,這里我們便可以關注requirejs方法的細節了:
① 這次調用首先會取出當前上下文環境,然后調用config方法設置屬性,最后調用context.require進行實際操作
context = getOwn(contexts, contextName); if (!context) { context = contexts[contextName] = req.s.newContext(contextName); } if (config) { context.configure(config); } return context.require(deps, callback, errback);
② 屬性配置結束后便執行localRequire方法,這里會慢慢加載模塊了,然后使用
requireMod = getModule(makeModuleMap(null, relMap));
真正加載模塊,具體里面真的干了什么,這就是我們今天要學習的內容了,於是我們繼續吧
requireJS如何加載data-main文件
這里我們先干一件事情,搞清楚requireJS是如何將script標簽中data-main對應的文件加載出來的,這一步搞懂了后,才能知道其它文件如何加載
首先,根據前面的學習,我們知道了
req(cfg) => context.require(config) => context.makeRequire() => localRequire()
而localRequire干了很多事情,我們這里暫時是不關注的,於是主要注意力一到了nextTick上面,於是最終進入了這里的核心getModule
getModule
function getModule(depMap) { var id = depMap.id, mod = getOwn(registry, id); if (!mod) { mod = registry[id] = new context.Module(depMap); } return mod; }
我們前面說了registry里面存儲着已經加載好了的模塊,而一個模塊加載后便不會再加載了,這里的唯一標識是makeModuleMap處理的,我們暫時不予關注
這里沒有便會創建,而這里的創建module過程便是我們需要了解的主干流程
mod = registry[id] = new context.Module(depMap);
context.Module
我們這里實例化了一個mod作為返回,並且將之存入了registry對象中,初始化過程中並未做什么特別的事情,但是該模塊具備了大量實用方法

然后,將該mod返回給了我們的臨時變量requireMod,並且調用了其初始化方法
requireMod.init(deps, callback, errback, { enabled: true });
requireMod.init
這里開始了我們實際的模塊初始化邏輯了

① 如果該模塊已經初始化便不再執行下面邏輯
② 然后他會將依賴映射取出,這里為了保存原始數組的完整性還做了其它操作
//Do a copy of the dependency array, so that //source inputs are not modified. For example //"shim" deps are passed in here directly, and //doing a direct modification of the depMaps array //would affect that config. this.depMaps = depMaps && depMaps.slice(0);
最后執行了module的enable方法,enable最后會調用this.check
requireMod.check
調用enable方法時,首先會存一個類似於registry的對象enabledRegistry,這個應該與依賴項什么的相關,比如backbone依賴於underscore,這里就應該先加載underscore
但是這里具體干了什么,我們先放一下,先跑主干流程,我們這里直接進入check
PS:這個enable很關鍵,我們后面一點詳細來看看
進入check流程后,馬上又跳至了this.fetch
這里首先記錄了當前上下文環境的開始時間(此context是所有mod共享的)
context.startTime = (new Date()).getTime();
然后回調用this.load方法,這個方法有可能要加載標簽了,這里也是有點東西的,我們稍候再說

然后控制器又回到了context的load手里
//Delegates to req.load. Broken out as a separate function to //allow overriding in the optimizer. load: function (id, url) { req.load(context, id, url); }, req.load = function (context, moduleName, url) { var config = (context && context.config) || {}, node; if (isBrowser) { //In the browser so use a script tag node = req.createNode(config, moduleName, url); node.setAttribute('data-requirecontext', context.contextName); node.setAttribute('data-requiremodule', moduleName); if (node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera) { useInteractive = true; node.attachEvent('onreadystatechange', context.onScriptLoad); } else { node.addEventListener('load', context.onScriptLoad, false); node.addEventListener('error', context.onScriptError, false); } node.src = url; currentlyAddingScript = node; if (baseElement) { head.insertBefore(node, baseElement); } else { head.appendChild(node); } currentlyAddingScript = null; return node; } else if (isWebWorker) { try { importScripts(url); context.completeLoad(moduleName); } catch (e) { context.onError(makeError('importscripts', 'importScripts failed for ' + moduleName + ' at ' + url, e, [moduleName])); } } };
這里做了很多兼容性處理,我們直接抽取主干邏輯即可,這里是重頭戲了:
① 使用createNode創建script標簽
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; };

這里創建標簽后給其注入了一些自定義屬性,並且綁定了一個事件(這里各個瀏覽器可能不同,我們關注標准的)
node.addEventListener('load', context.onScriptLoad, false);
onScriptLoad: function (evt) { if (evt.type === 'load' || (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { interactiveScript = null; var data = getScriptData(evt); context.completeLoad(data.id); } },
然后將該節點插入head中
head.appendChild(node);
於是頭部多出了這么一塊東西

此段js加載結束后馬上執行調用onScriptLoad方法,但是,我們寫在main.js中的邏輯會被調用,該函數暫時看來是執行一些資源清理工作
並且會將該節點存儲起來
PS:注意啦,這里可能會有一定時序性問題,整個這塊邏輯我們還需要整理
看到這里,我們基本明白了,我們第一個文件,main.js是如何加載出來的了
一個簡單的例子
為了幫助理解,我們這里以一個簡單的例子做說明,再看看源碼的實現
HTML
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> <script src="require.js" type="text/javascript" data-main="main.js"></script> </body> </html>
main.js
require.config({ baseUrl: '', paths: { 'nameDep': 'js/nameDep', 'say': 'js/say', 'name': 'js/name' }, shim: { 'name': { deps: ['nameDep'] } } }); require(['name', 'say'], function (name, say) { say(name); });
name/say
//name
define([''], function () { return '測試'; });
//say define([], function () { return function (name) { console.log(name); }; });
由上面的邏輯來看,當main.js加載結束后於是就該進行下一個邏輯的處理,我們這里看看他是如何處理的呢,這個時候第一個入口便是require.config了
require.config
這個時候會傳一些有意義的參數進來了

整個config依舊在做參數設置,這個這個操作實際的意義是為newContext中的config賦值,並在localRequire中使用

接下來順理成章的再次進入了模塊實例化的步驟
requireMod = getModule(makeModuleMap(null, relMap));
最后進入上面提到的初始化環節,但是內部會有不一樣的細節處理(我們暫時不予關注,留着下次學習)這里每一次加載會清除前面的registry
config的設置並不會觸發script標簽的加載,所以其實際加載還是下面的require,這塊的邏輯又有點小復雜了......

PS:這里設置斷點調試時序上好像有點問題,所以這塊暫時就不處理了,應該與其維護的隊列有關,留待下次解決吧
結語
通過這兩天的學習,我們大概了解了requireJS的一些東西,但是還是那句話,小釵感覺requireJS讀起來還是有點小難,不是那么好吸收
關於他的學習,還需要慢慢來哦,感覺里面水有點深......
