【源碼學習】之requirejs
對於現在的前端生態來說,requirejs是有點過時了,webpack幫我們包幹了一切。但是對於學習源碼這件事情來說,永遠是不過時的!
最近稍微閑下來了一點,就著以前做過的項目,我也來看看requirejs的源碼。希望能漲點姿勢!
1.html中的data-main是個什麽鬼?
//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在哪裏出現了。
//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處匹配的地方,全部在上面這段代碼中.
這裏用到了一個公有方法eachReverse,包含兩個參數,ary和func,func是回調函數,回調函數接受三個參數(數組的每一項,數組的索引,完整的數組元素).
/** * 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屬性。
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({});初始化執行上下文以後才會需要用到。這裏只是按照我們正常思維打斷點先想到的。
2.js裏面怎麽跑
註釋上寫到這裏是程序的主入口,相當於構造函數,那我們就來看一下。
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);
3.小結一下
req({}) => req(cfg);
這一段流程走過以後,我們發現最大的改變就是contexts這個對象。
=>
而這些改變最重要的目的就是創建一個適合require運行的上下文環境。當然通過makeRequire創建的閉包函數ocalRequire,它也是不同的,因為後面的邏輯不同,傳入的參數不同,形成了不同的閉包。
這幾天require讀下來,感覺沒那麽好懂,果然還是水平不夠,先好好消化一下。下次再來繼續啃.
【源碼學習】之requirejs