requirejs
做为AMD
(Asynchronous Module Definition--异步的模块加载机制)规范的实现,仍是有必要看看的。初识requirejs
源码,必须先弄清楚requirejs
的模块是如何定义的,而且要知道入口在哪一个地方,若是清楚了调用方式,看源码的时候才会以为顺畅。javascript
在看源码的过程当中,我添加了一些代码注释。若是要查看添加过注释的源码,能够直接在个人github上进行fork。我这里的源码是目前最新的版本2.3.5。另外附上requirejs官方的源码。html
我把requirejs一共分红了三个部分,这三个部分外面是一个闭包,而且两个定义的全局变量。html5
var requirejs, require, define;
(function (global, setTimeout) {
//一、定义一些变量与工具方法
var req, s, head ////some defined
//add some function
//二、建立一个模块加载的上下文
function newContext(contextName) {
//somecode
//定义一个模块加载器
Module = function (map) {}
Module.prototype = {
//原型链上
};
context = { //上下文环境
config: config, //配置
contextName: contextName, //默认为 "_"
nextTick: req.nextTick, //经过setTimeout,把执行放到下一个队列
makeRequire: function (relMap, options) {
function localRequire () {
//somecode
//经过setTimeout的方式加载依赖,放入下一个队列,保证加载顺序
context.nextTick(function () {
intakeDefines();
requireMod = getModule(makeModuleMap(null, relMap));
requireMod.skipMap = options.skipMap;
requireMod.init(deps, callback, errback, {
enabled: true
});
checkLoaded();
});
return localRequire;
}
return localRequire;
}
//xxxx
}
context.require = context.makeRequire(); //加载时的入口函数
return context;
}
//三、定义require、define方法,导入data-main路径与进行模块加载
req = requirejs = function (deps, callback, errback, optional) {
//xxxx
context = getOwn(contexts, contextName); //获取默认环境
if (!context) {
context = contexts[contextName] = req.s.newContext(contextName); //建立一个名为'_'的环境名
}
if (config) {
context.configure(config); //设置配置
}
return context.require(deps, callback, errback);
}
req.config = function (config) {
return req(config);
};
s = req.s = {
contexts: contexts,
newContext: newContext
};
req({}); //初始化模块加载的上下文环境
define = function (name, deps, callback) {
}
req(cfg); //加载data-main,主入口js
}(this, (typeof setTimeout === 'undefined' ? undefined : setTimeout)));
复制代码
上面的代码基本能看出requirejs
的三个部分,中间省略了不少代码。看过大概结构以后,来跟着我一步一步的窥探requirejs
是如何加载与定义模块的。java
使用过requirejs的朋友都知道,咱们会在引入requirejs的时候,在script
标签添加data-main
属性,做为配置和模块加载的入口。具体代码以下:node
<script type="text/javascript" src="./require.js" data-main="./js/main.js"></script>
复制代码
requirejs
先经过判断当前是否为浏览器环境,若是是浏览器环境,就遍历当前页面上全部的script标签,取出其中的data-main
属性,并经过计算,获得baseUrl和须要提早加载js的文件名。具体代码以下:git
var isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document);
function scripts() { //获取页面上全部的target标签
return document.getElementsByTagName('script');
}
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;
}
}
}
}
if (isBrowser) {
head = s.head = document.getElementsByTagName('head')[0];
baseElement = document.getElementsByTagName('base')[0];
if (baseElement) {
head = s.head = baseElement.parentNode;
}
}
if (isBrowser && !cfg.skipDataMain) {
eachReverse(scripts(), function (script) { //遍历全部的script标签
//若是head标签不存在,让script标签的父节点充当head
if (!head) {
head = script.parentNode;
}
dataMain = script.getAttribute('data-main');
if (dataMain) { //获取data-main属性(若是存在)
//保存dataMain变量,防止转换后任然是路径 (i.e. contains '?')
mainScript = dataMain;
//若是没有指定明确的baseUrl,设置data-main属性的路径为baseUrl
//只有当data-main的值不为一个插件的模块ID时才这样作
if (!cfg.baseUrl && mainScript.indexOf('!') === -1) {
//取出data-main中的路径做为baseUrl
src = mainScript.split('/'); //经过 / 符,进行路径切割
mainScript = src.pop(); //拿出data-main中的js名
subPath = src.length ? src.join('/') + '/' : './'; //拼接父路径,若是data-main只有一个路径,则表示当前目录
cfg.baseUrl = subPath;
}
//去除js后缀,做模块名
mainScript = mainScript.replace(jsSuffixRegExp, '');
//若是mainScript依旧是一个路径, 将mainScript重置为dataMain
if (req.jsExtRegExp.test(mainScript)) {
mainScript = dataMain;
}
//将data-main的模块名放入到deps数组中
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
return true;
}
});
}
复制代码
在进行过上述操做后,咱们能够获得一个cfg对象,该对象包括两个属性baseUrl和deps。好比咱们上面的案例中,script标签有个属性data-main="./js/main.js"
,通过requirejs的转后,获得的cfg对象为:angularjs
cfg = {
baseUrl: "./js/",
deps: ["main"]
}
复制代码
拿到cfg对象后,requirejs调用了req方法:req(cfg);
。req方法就是require方法,是整个requirejs的入口函数,至关因而一个分发装置,进行参数类型的匹配,再来判断当前是config操做仍是require操做,而且在这个方法里还会建立一个上下文环境,全部的模块加载和require相关的配置都会在这个上下文进行中进行。在调用req(cfg);
以前,requirejs还调用了一次req方法:req({});
,这一步操做就是为了建立模块加载的上下文。咱们还在直接来看看req方法的源码吧:github
//最开始定义的变量
var defContextName = '_', //默认加载的模块名
contexts = {}; //模块加载的上下文环境的容器
req = requirejs = function (deps, callback, errback, optional) {
//Find the right context, use default
var context, config,
contextName = defContextName; //默认的上下文环境
//参数修正
// Determine if have config object in the call.
if (!isArray(deps) && typeof deps !== 'string') {
// deps is a config object
config = deps; //第一个参数若是不是数组也不是字符串表示为配置参数
if (isArray(callback)) {
// 调整参数,callback此时是deps
deps = callback;
callback = errback;
errback = optional;
} else {
deps = [];
}
}
if (config && config.context) {
contextName = config.context;
}
context = getOwn(contexts, contextName); //获取默认环境
if (!context) { //若是是第一次进入,调用newContext方法进行建立
context = contexts[contextName] = req.s.newContext(contextName); //建立一个名为'_'的环境名
}
if (config) {
context.configure(config); //设置配置
}
//若是只是加载配置,deps、callback、errback这几个参数都是空,那么调用require方法什么都不会发生
return context.require(deps, callback, errback); //最后调用context中的require方法,进行模块加载
};
req.config = function (config) {
return req(config); //require.config方法最终也是调用req方法
};
if (!require) { //require方法就是req方法
require = req;
}
s = req.s = {
contexts: contexts,
newContext: newContext //建立新的上下文环境
};
复制代码
继续按照以前req(cfg);
的逻辑来走,根据传入的cfg,会调用context.configure(config);
,而这个context就是以前说的requirejs
三部分中的第二个部分的newContext
函数建立的,建立获得的context对象会放入全局的contexts对象中。咱们能够在控制台打印contexts对象,看到里面其实只有一个名为'_'
的context,这是requrejs
默认指定的上下文。web
newContext函数中有许多的局部变量用来缓存一些已经加载的模块,还有一个模块加载器(Module),这个后面都会用到。仍是先看调用的configure方法:数组
function newContext (contextName) {
var context, config = {};
context = {
configure: function (cfg) {
//确保baseUrl以 / 结尾
if (cfg.baseUrl) {
//全部模块的根路径,
//默认为requirejs的文件所在路径,
//若是设置了data-main,则与data-main一致
if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
cfg.baseUrl += '/';
}
}
//其余代码,用于添加一些替他配置,与本次加载无关
//若是配置项里指定了deps或者callback, 则调用require方法
//若是实在requirejs加载以前,使用require定义对象做为配置,这颇有用
if (cfg.deps || cfg.callback) {
context.require(cfg.deps || [], cfg.callback);
}
},
makeRequire: function (relMap, options) {
}
}
return context;
}
复制代码
这个方法主要是用来作配置,在咱们传入的cfg参数中其实并不包含requirejs的主要配置项,可是在最后由于有deps属性,逻辑能继续往下走,调用了require方法:context.require(cfg.deps);
。上面的代码中能看出,context的require方法是使用makeRequire建立的,这里之因此用makeRequire来建立require方法,主要使用建立一个函数做用域来保存,方便为require方法拓展一些属性。
context = {
makeRequire: function (relMap, options) {
options = options || {};
function localRequire(deps, callback, errback) { //真正的require方法
var id, map, requireMod;
if (options.enableBuildCallback && callback && isFunction(callback)) {
callback.__requireJsBuild = true;
}
if (typeof deps === 'string') {
//若是deps是个字符串,而不是个数组,进行一些其余处理
}
intakeDefines();
//经过setTimeout的方式加载依赖,放入下一个队列,保证加载顺序
context.nextTick(function () {
intakeDefines();
requireMod = getModule(makeModuleMap(null, relMap));
requireMod.skipMap = options.skipMap;
requireMod.init(deps, callback, errback, {
enabled: true
});
checkLoaded();
});
return localRequire;
}
//mixin类型与extend方法,对一个对象进行属性扩展
mixin(localRequire, {
isBrowser,
isUrl,
defined,
specified
});
return localRequire;
}
};
context.require = context.makeRequire(); //加载时的入口函数
复制代码
最初我是使用打断点的方式来阅读源码的,每次在看到context.nextTick
的以后,就没有往下进行了,百思不得其解。而后我看了看nextTick究竟是用来干吗的,发现这个方法其实就是个定时器。
context = {
nextTick: req.nextTick, //经过setTimeout,把执行放到下一个队列
};
req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
setTimeout(fn, 4);
} : function (fn) { fn(); };
复制代码
我也很费解,为何要把一些主逻辑放入到一个定时器中,这样全部的加载都会放到下一个任务队列进行。查看了requirejs的版本迭代,发现nextTick是在2.10这个版本加入的,以前也没有这个逻辑。 并且就算我把requirejs源码中的nextTick这段逻辑去除,代码也能正常运行。
tips:
这里的setTimeout之因此设置为4ms,是由于html5规范中规定了,setTimeout的最小延迟时间(DOM_MIN_TIMEOUT_VALUE
)时,这个时间就是4ms。可是在2010年以后,全部浏览器的实现都遵循这个规定,2010年以前为10ms。
后来参考了网络上其余博客的一些想法,有些人认为设置setTimeout来加载模块是为了让模块的加载是按照顺序执行的,这个目前我也没研究透彻,先设个。todo
在这里,哈哈哈
终于在requirejs的wiki上看到了相关文档,官方说法是为了让模块的加载异步化,为了防止一些细微的bug(具体是什么bug,还不是很清楚)。
好了,仍是继续来看requirejs
的源码吧。在nextTick中,首先使用makeModuleMap来构造了一个模块映射, 而后马上经过getModule新建了一个模块加载器。
//requireMod = getModule(makeModuleMap(null, relMap)); //nextTick中的代码
//建立模块映射
function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
var url, pluginModule, suffix, nameParts,
prefix = null,
parentName = parentModuleMap ? parentModuleMap.name : null,
originalName = name,
isDefine = true, //是不是define的模块
normalizedName = '';
//若是没有模块名,表示是require调用,使用一个内部名
if (!name) {
isDefine = false;
name = '_@r' + (requireCounter += 1);
}
nameParts = splitPrefix(name);
prefix = nameParts[0];
name = nameParts[1];
if (prefix) { //若是有插件前缀
prefix = normalize(prefix, parentName, applyMap);
pluginModule = getOwn(defined, prefix); //获取插件
}
if (name) {
//对name再进行一些特殊处理
}
return {
prefix: prefix,
name: normalizedName,
parentMap: parentModuleMap,
unnormalized: !!suffix,
url: url,
originalName: originalName,
isDefine: isDefine,
id: (prefix ?
prefix + '!' + normalizedName :
normalizedName) + suffix
};
}
//获取一个模块加载器
function getModule(depMap) {
var id = depMap.id,
mod = getOwn(registry, id);
if (!mod) { //对未注册模块,添加到模块注册器中
mod = registry[id] = new context.Module(depMap);
}
return mod;
}
//模块加载器
Module = function (map) {
this.events = getOwn(undefEvents, map.id) || {};
this.map = map;
this.shim = getOwn(config.shim, map.id);
this.depExports = [];
this.depMaps = [];
this.depMatched = [];
this.pluginMaps = {};
this.depCount = 0;
/* this.exports this.factory this.depMaps = [], this.enabled, this.fetched */
};
Module.prototype = {
init: function () {},
fetch: function () {},
load: function () {},
callPlugin: function () {},
defineDep: function () {},
check: function () {},
enable: function () {},
on: function () {},
emit: function () {}
};
复制代码
requireMod.init(deps, callback, errback, {
enabled: true
});
复制代码
拿到建立的模块加载器以后,当即调用了init方法。init方法中又调用了enable方法,enable方法中为全部的depMap又从新建立了一个模块加载器,并调用了依赖项的模块加载器的enable方法,最后调用check方法,check方法又立刻调用了fetch方法,fatch最后调用的是load方法,load方法迅速调用了context.load方法。千言万语不如画张图。
确实这一块的逻辑很绕,中间每一个方法都对一些做用域内的参数有一些修改,先只了解大体流程,后面慢慢讲。 这里重点看下req.load方法,这个方法是全部模块进行加载的方法。
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; //建立script标签添加了async属性
return node;
};
req.load = function (context, moduleName, url) { //用来进行js模块加载的方法
var config = (context && context.config) || {},
node;
if (isBrowser) { //在浏览器中加载js文件
node = req.createNode(config, moduleName, url); //建立一个script标签
node.setAttribute('data-requirecontext', context.contextName); //requirecontext默认为'_'
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;
if (config.onNodeCreated) { //script标签建立时的回调
config.onNodeCreated(node, config, moduleName, url);
}
currentlyAddingScript = node;
if (baseElement) { //将script标签添加到页面中
head.insertBefore(node, baseElement);
} else {
head.appendChild(node);
}
currentlyAddingScript = null;
return node;
} else if (isWebWorker) { //在webWorker环境中
try {
setTimeout(function () { }, 0);
importScripts(url); //webWorker中使用importScripts来加载脚本
context.completeLoad(moduleName);
} catch (e) { //加载失败
context.onError(makeError('importscripts',
'importScripts failed for ' +
moduleName + ' at ' + url,
e,
[moduleName]));
}
}
};
复制代码
requirejs加载模块的方式是经过建立script标签进行加载,而且将建立的script标签插入到head中。并且还支持在webwork中使用,在webWorker使用importScripts()
来进行模块的加载。
最后能够看到head标签中多了个script:
requirejs提供了模块定义的方法:define
,这个方法遵循AMD规范,其使用方式以下:
define(id?, dependencies?, factory);
复制代码
define三个参数的含义以下:
factory也支持commonjs的方式来定义模块,若是define没有传入依赖数组,factory会默认传入三个参数require, exports, module
。 没错,这三个参数与commonjs对应的加载方式保持一致。require用来引入模块,exports和module用来导出模块。
//写法1:
define(
['dep1'],
function(dep1){
var mod;
//...
return mod;
}
);
//写法2:
define(
function (require, exports, module) {
var dep1 = require('dep1'), mod;
//...
exports = mod;
}
});
复制代码
废话很少说,咱们仍是直接来看源码吧!
/** * 用来定义模块的函数。与require方法不一样,模块名必须是第一个参数且为一个字符串, * 模块定义函数(callback)必须有一个返回值,来对应第一个参数表示的模块名 */
define = function (name, deps, callback) {
var node, context;
//运行匿名模块
if (typeof name !== 'string') {
//参数的适配
callback = deps;
deps = name;
name = null;
}
//这个模块能够没有依赖项
if (!isArray(deps)) {
callback = deps;
deps = null;
}
//若是没有指定名字,而且callback是一个函数,使用commonJS形式引入依赖
if (!deps && isFunction(callback)) {
deps = [];
//移除callback中的注释,
//将callback中的require取出,把依赖项push到deps数组中。
//只在callback传入的参数不为空时作这些
if (callback.length) { //将模块的回调函数转成字符串,而后进行一些处理
callback
.toString()
.replace(commentRegExp, commentReplace) //去除注释
.replace(cjsRequireRegExp, function (match, dep) {
deps.push(dep); //匹配出全部调用require的模块
});
//兼容CommonJS写法
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) { //ie 6-8 进行特殊处理
node = currentlyAddingScript || getInteractiveScript();
if (node) {
if (!name) {
name = node.getAttribute('data-requiremodule');
}
context = contexts[node.getAttribute('data-requirecontext')];
}
}
//若是存在context将模块放到context的defQueue中,不存在contenxt,则把定义的模块放到全局的依赖队列中
if (context) {
context.defQueue.push([name, deps, callback]);
context.defQueueMap[name] = true;
} else {
globalDefQueue.push([name, deps, callback]);
}
};
复制代码
经过define定义模块最后都会放入到globalDefQueue数组中,当前上下文的defQueue数组中。具体怎么拿到定义的这些模块是使用takeGlobalQueue
来完成的。
/** * 内部方法,把globalQueue的依赖取出,放到当前上下文的defQueue中 */
function intakeDefines() { //获取并加载define方法添加的模块
var args;
//取出全部define方法定义的模块(放在globalqueue中)
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);
}
}
context.defQueueMap = {};
}
function takeGlobalQueue() {
//将全局的DefQueue添加到当前上下文的DefQueue
if (globalDefQueue.length) {
each(globalDefQueue, function (queueItem) {
var id = queueItem[0];
if (typeof id === 'string') {
context.defQueueMap[id] = true;
}
defQueue.push(queueItem);
});
globalDefQueue = [];
}
}
//intakeDefines()方法是在makeRequire中调用的
makeRequire: function (relMap, options) { //用于构造require方法
options = options || {};
function localRequire(deps, callback, errback) { //真正的require方法
intakeDefines();
context.nextTick(function () {
//Some defines could have been added since the
//require call, collect them.
intakeDefines();
}
}
}
//同时依赖被加载完毕的时候也会调用takeGlobalQueue方法
//以前咱们提到requirejs是向head头中insert一个script标签的方式加载模块的
//在加载模块的同时,为script标签绑定了一个load事件
node.addEventListener('load', context.onScriptLoad, false);
//这个事件最后会调用completeLoad方法
onScriptLoad: function (evt) {
if (evt.type === 'load' ||
(readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
var data = getScriptData(evt);
context.completeLoad(data.id);
}
}
completeLoad: function (moduleName) {
var found;
takeGlobalQueue();//获取加载的js中进行define的模块
while (defQueue.length) {
args = defQueue.shift();
if (args[0] === null) {
args[0] = moduleName;
if (found) {
break;
}
found = true;
} else if (args[0] === moduleName) {
found = true;
}
callGetModule(args);
}
context.defQueueMap = {};
}
复制代码
不管是经过require的方式拿到defie定义的模块,仍是在依赖加载完毕后,经过scriptLoad事件拿到定义的模块,这两种方式最后都使用callGetModule()
这个方法进行模块加载。下面咱们仍是详细看看callGetModule以后,都发生了哪些事情。
function callGetModule(args) {
//跳过已经加载的模块
if (!hasProp(defined, args[0])) {
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
}
}
复制代码
其实callGetModule方法就是调用了getModule方法(以前已经介绍过了),getModule方法返回一个Module(模块加载器)实例,最后调用实例的init方法。init方法会调用check方法,在check方法里会执行define方法所定义的factory,最后将模块名与模块保存到defined全局变量中。
exports = context.execCb(id, factory, depExports, exports);
defined[id] = exports;
复制代码
到这里定义模块的部分已经结束了。这篇文章先写到这儿,这里只理清了模块的定义和requirejs的初次加载还有requirejs的入口js是如何引入的,这一部分不少细节都没有讲到。本身挖个坑在这儿,下一部分会深刻讲解Module模块加载器的构成,还有require方法是如何引入依赖的。
下期再见。