對於如今的前端生態來講,requirejs是有點過期了,webpack幫咱們包乾了一切。可是對於學習源碼這件事情來講,永遠是不過期的!javascript
最近稍微閒下來了一點,就着之前作過的項目,我也來看看requirejs的源碼。但願能漲點姿式!html
//address.html
<script type="text/javascript" data-main="${base}/static/js/app/userCenter/address" src="${base}/static/js/plugins/require.js"></script>
使用requirejs,在咱們的頁面須要引入一個有data-main的主入口js文件。前端
既然這樣,咱們就去require源碼中去找找data-main在哪裏出現了。java
//Look for a data-main script attribute, which could also adjust the baseUrl.去尋找一個data-main的script屬性,而且可以匹配baseUrl if (isBrowser && !cfg.skipDataMain) { //Figure out baseUrl. Get it from the script tag with require.js in it.計算出baseUrl.從含有require.js的script標籤中獲取它. eachReverse(scripts(), function (script) { //Set the 'head' where we can append children by //using the script's parent. if (!head) { head = script.parentNode; } //Look for a data-main attribute to set main script for the page //to load. If it is there, the path to data main becomes the //baseUrl, if it is not already set. dataMain = script.getAttribute('data-main'); if (dataMain) { //Preserve dataMain in case it is a path (i.e. contains '?') mainScript = dataMain; //Set final baseUrl if there is not already an explicit one, //but only do so if the data-main value is not a loader plugin //module ID. if (!cfg.baseUrl && mainScript.indexOf('!') === -1) { //Pull off the directory of data-main for use as the //baseUrl. src = mainScript.split('/'); mainScript = src.pop(); subPath = src.length ? src.join('/') + '/' : './'; cfg.baseUrl = subPath; } //Strip off any trailing .js since mainScript is now //like a module name. mainScript = mainScript.replace(jsSuffixRegExp, ''); //If mainScript is still a path, fall back to dataMain if (req.jsExtRegExp.test(mainScript)) { mainScript = dataMain; } //Put the data-main script in the files to load. cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; return true; } }); }
咱們在源碼中找到了6處匹配的地方,所有在上面這段代碼中.webpack
這裏用到了一個公有方法eachReverse,包含兩個參數,ary和func,func是回調函數,回調函數接受三個參數(數組的每一項,數組的索引,完整的數組元素).web
/** * Helper function for iterating over an array backwards. If the func 幫助函數爲了倒序遍歷數組,若是func返回true,則跳出循環 * returns a true value, it will break out of the loop. */ function eachReverse(ary, func) { if (ary) { var i; for (i = ary.length - 1; i > -1; i -= 1) { if (ary[i] && func(ary[i], i, ary)) { break; } } } }
eachReverse的ary是一個scripts()方法返回的數組。因此接下來去看看scripts方法。scripts方法取到html上全部的script標籤.編程
function scripts() { return document.getElementsByTagName('script'); }
經過script取到data-main屬性的值。咱們能夠看到dataMain變量的值就是address.html中data-main屬性的值。數組
接下來的操做都是對url地址的一些處理.經過src.pop()取得mainScript的值爲address.閉包
再將地址拼接起來取得子目錄。能夠看到subPath少了前面的/address目錄,subPath被賦值給了cfg.baseUrl屬性。app
jsSuffixRegExp = /\.js$/, //Strip off any trailing .js since mainScript is now //like a module name. //剝去任何.js結尾的mainScript,使得它看起來像一個模塊的名稱 mainScript = mainScript.replace(jsSuffixRegExp, '');
經過正則匹配任何已.js結尾的文件。例如上面的address.html的data-main若是變成:xxxxx/address.js ,這裏就會把.js給替換掉,如同註釋中字面意義的「模塊化」。
到這裏的話,對data-main的處理算完結了。正如data-main是咱們的主模塊,address.html的主模塊就是deps裏的address。
可是要說一點的就是這裏的cfg對象是要在req({});初始化執行上下文之後纔會須要用到。這裏只是按照咱們正常思惟打斷點先想到的。
註釋上寫到這裏是程序的主入口,至關於構造函數,那咱們就來看一下。
1 /** 2 * Main entry point.主入口 3 * 4 * If the only argument to require is a string, then the module that 5 * is represented by that string is fetched for the appropriate context. 6 * 7 * If the first argument is an array, then it will be treated as an array 8 * of dependency string names to fetch. An optional function callback can 9 * be specified to execute when all of those dependencies are available. 10 * 11 * Make a local req variable to help Caja compliance (it assumes things 建立一個局部req變量去幫助caja compliance,這個caja貌似說的是一個google的caja庫,相似建立了一個虛擬的iframe,而且給一個短名稱的局部做用域去使用。 12 * on a require that are not standardized), and to give a short 13 * name for minification/local scope use. 14 */ 15 req = requirejs = function (deps, callback, errback, optional) { 16 17 //Find the right context, use default 18 var context, config, 19 contextName = defContextName; 20 21 // Determine if have config object in the call. 22 if (!isArray(deps) && typeof deps !== 'string') { 23 // deps is a config object deps是一個配置對象 24 config = deps; 25 if (isArray(callback)) { 26 // Adjust args if there are dependencies 27 deps = callback; 28 callback = errback; 29 errback = optional; 30 } else { 31 deps = []; 32 } 33 } 34 35 if (config && config.context) { 36 contextName = config.context; 37 } 38 39 context = getOwn(contexts, contextName); 40 if (!context) { 41 context = contexts[contextName] = req.s.newContext(contextName); 42 } 43 44 if (config) { 45 context.configure(config); 46 } 47 48 return context.require(deps, callback, errback); 49 };
在隨後的代碼中,執行了req而且傳入一個空對象,這裏就建立了req這個函數執行的上下文。
//Create default context. req({});
這裏用到了getOwn函數,getOwn要配合hasProp使用。先檢查是否包含實例屬性,若是包含的話就將屬性賦值到目標對象。
1 function hasProp(obj, prop) { 2 return hasOwn.call(obj, prop); 3 } 4 5 function getOwn(obj, prop) { 6 return hasProp(obj, prop) && obj[prop]; 7 }
由於context爲false,因此newContext進行了初始化。
1 s = req.s = { 2 contexts: contexts, 3 newContext: newContext 4 };
newContext的代碼很是的多,差很少1500行左右。
newContext大體結構以下:
1.一些工具方法:例如trimDots。
2.處理模塊的方法:例如normalize等
3.建立並保存了require的運行環境:context對象中的方法
4.建立了require的模塊:Module構造函數
這裏context對象調用了makeRequire方法。
context.require = context.makeRequire(); return context;
1 //簡化後的代碼,能夠很明顯的看出,爲了造成閉包 2 makeRequire:function(){ 3 function localRequire(){ 4 //TODO 5 return localRequire; 6 } 7 return localRequire; 8 }
經過一個mixin方法實現了屬性拷貝。
1 /** 2 * Simple function to mix in properties from source into target, 簡單的方法把源對象的屬性混合進目標對象中,僅在目標對象並無相同屬性名稱的狀況下 3 * but only if target does not already have a property of the same name. 4 */ 5 function mixin(target, source, force, deepStringMixin) { 6 if (source) { 7 eachProp(source, function (value, prop) { 8 if (force || !hasProp(target, prop)) { 9 if (deepStringMixin && typeof value === 'object' && value && 10 !isArray(value) && !isFunction(value) && 11 !(value instanceof RegExp)) { 12 13 if (!target[prop]) { 14 target[prop] = {}; 15 } 16 mixin(target[prop], value, force, deepStringMixin); 17 } else { 18 target[prop] = value; 19 } 20 } 21 }); 22 } 23 return target; 24 }
最後返回的target,也就是咱們localRequire,添加了4個屬性,這裏咱們能夠看出來,它是返回了函數localRequire的閉包。
又給localRequire這個閉包再添加了一個屬性,undef
並將閉包賦值給context.require。隨後返回context這個對象。
而後咱們會進入configure這個方法,由於第一次初始化是傳入的一個空對象,因此這裏對配置的處理並無什麼實際意義,咱們暫且略過。在第二次有具體參數傳入了再具體說明。
最後將在context對象中維護的localRequire閉包執行並返回。
return context.require(deps, callback, errback);
咱們會碰到nextTick這樣一個方法,req.nextTick將匿名函數添加到事件隊列中去,異步的去執行它,而這裏的匿名函數的功能就是去異步的加載require的模塊。可是爲什麼這裏與前一次異步延時設置爲4,我以爲1,2,3應該都是能夠的,這裏不是很清楚!若是有朋友瞭解,能夠解釋一下
不過這裏的註釋仍是很可笑的:若是有比setTimeout更好的方法,那麼就去重寫它。而後用的名稱叫nextTick,就是在Node中爲了解決setTimeout存在問題的方法。你們有興趣的話能夠去看看《異步編程》。
1 /** 2 * Execute something after the current tick 3 * of the event loop. Override for other envs 4 * that have a better solution than setTimeout. 5 * @param {Function} fn function to execute later. 6 */ 7 req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { 8 setTimeout(fn, 4); 9 } : function (fn) { fn(); };
繼續往下走,咱們看到了經過mixin方法添加到閉包的4個屬性,這裏把這4個屬性給暴露給了外層的req對象。
1 //Exports some context-sensitive methods on global require. 2 each([ 3 'toUrl', 4 'undef', 5 'defined', 6 'specified' 7 ], function (prop) { 8 //Reference from contexts instead of early binding to default context, 9 //so that during builds, the latest instance of the default context 10 //with its config gets used. 11 req[prop] = function () { 12 var ctx = contexts[defContextName]; 13 return ctx.require[prop].apply(ctx, arguments); 14 }; 15 });
隨後會執行我前面提到的處理data-main這塊的代碼。當全部的準備工做作好了之後,
在這裏就將咱們前面經過data-main拿到的cfg對象傳進去。
1 //Set up with config info. 2 req(cfg);
req({}) => req(cfg);
這一段流程走過之後,咱們發現最大的改變就是contexts這個對象。
=>
而這些改變最重要的目的就是建立一個適合require運行的上下文環境。固然經過makeRequire建立的閉包函數ocalRequire,它也是不一樣的,由於後面的邏輯不一樣,傳入的參數不一樣,造成了不一樣的閉包。
這幾天require讀下來,感受沒那麼好懂,果真仍是水平不夠,先好好消化一下。下次再來繼續啃.