对于如今的前端生态来讲,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读下来,感受没那么好懂,果真仍是水平不够,先好好消化一下。下次再来继续啃.