模块加载器源码分析之requiresJs篇(二)
Opened this issue · 0 comments
前言
我在第一篇中讲解了如何使用requirejs,以及将requirejs下载下来经历的几个步骤,算是对整体结构进行了一层剖析,在这一篇章中,我会对requirejs的接下几个流程做一些分析。
requirejs的执行过程
data-main入口点
昨天提到一旦载入requirejs就会执行require(cfg),而在设置了data-main="xxx.js"之后,requireJs将会把baserUrl设置为入口文件的所在目录,然后把deps的值设为mainScript,即xxx.js的值。具体代码:
if (isBrowser && !cfg.skipDataMain) {
//Figure out baseUrl. Get it from the script tag with require.js in it.
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;
}
});
}
然后我们之前提到的context.configure(cfg),在cfg.deps不为空的时候,会执行:
if (cfg.deps || cfg.callback) {
context.require(cfg.deps || [], cfg.callback);
}
也就是说会执行两次context.require,至于为什么要这么做,目前我还是不理解,我认为当下载下来可以通过判断来确定是否执行context.require,毕竟执行一次这个函数是有一定成本的。接下来会做昨天提到的几件事,但是这次有了依赖而不是空值,会真正去执行fectch函数了,这里说明一下执行流程:凡是执行了init函数的模块,不会再去fetch,即一是require模块本身不会去fetch,二是执行了define后的模块不会去fetch,而在enable递归中得到的依赖模块会执行fetch
详解依赖加载
fetch
代码:
fetch: function () {
if (this.fetched) {
return;
}
this.fetched = true;
context.startTime = (new Date()).getTime();
var map = this.map;
//console.log(map);
//If the manager is for a plugin managed resource,
//ask the plugin to load it now.
if (this.shim) {
context.makeRequire(this.map, {
enableBuildCallback: true
})(this.shim.deps || [], bind(this, function () {
return map.prefix ? this.callPlugin() : this.load();
}));
} else {
//Regular dependency.
return map.prefix ? this.callPlugin() : this.load();
}
},
在fetch中会执行load函数,即开始生成script元素并append到header下面,代码:
//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);
//Set up load listener. Test attachEvent first because IE9 has
//a subtle issue in its addEventListener and script onload firings
//that do not match the behavior of all other browsers with
//addEventListener support, which fire the onload event for a
//script right after the script execution. See:
//https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
//UNFORTUNATELY Opera implements attachEvent but does not follow the script
//script execution mode.
if (node.attachEvent &&
//Check if node.attachEvent is artificially added by custom script or
//natively supported by browser
//read https://github.com/requirejs/requirejs/issues/187
//if we can NOT find [native code] then it must NOT natively supported.
//in IE8, node.attachEvent does not have toString()
//Note the test for "[native code" with no closing brace, see:
//https://github.com/requirejs/requirejs/issues/273
!(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
!isOpera) {
//Probably IE. IE (at least 6-8) do not fire
//script onload right after executing the script, so
//we cannot tie the anonymous define call to a name.
//However, IE reports the script as being in 'interactive'
//readyState at the time of the define call.
useInteractive = true;
node.attachEvent('onreadystatechange', context.onScriptLoad);
//It would be great to add an error handler here to catch
//404s in IE9+. However, onreadystatechange will fire before
//the error handler, so that does not help. If addEventListener
//is used, then IE will fire error before load, but we cannot
//use that pathway given the connect.microsoft.com issue
//mentioned above about not doing the 'script execute,
//then fire the script load event listener before execute
//next script' that other browsers do.
//Best hope: IE10 fixes the issues,
//and then destroys all installs of IE 6-9.
//node.attachEvent('onerror', context.onScriptError);
} else {
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
}
node.src = url;
//Calling onNodeCreated after all properties on the node have been
//set, but before it is placed in the DOM.
if (config.onNodeCreated) {
config.onNodeCreated(node, config, moduleName, url);
}
//For some cache cases in IE 6-8, the script executes before the end
//of the appendChild execution, so to tie an anonymous define
//call to the module name (which is stored on the node), hold on
//to a reference to this node, but clear after the DOM insertion.
currentlyAddingScript = node;
if (baseElement) {
head.insertBefore(node, baseElement);
} else {
head.appendChild(node);
}
currentlyAddingScript = null;
return node;
注意到两件事,一是node中加上了asnyc属性,意味着js文件将异步加载和异步执行(前提是ie10以上的版本 ),二是为标签加上了监听事件,当下载并执行完js文件后,在执行completeLoad函数。在这里,必须重点说明下js中所执行的函数define。
####define
define函数是我之前提过的两大核心函数之一,它的作用和执非常隐蔽但又很关键,先来看看它的代码:
define = function (name, deps, callback) {
var node, context;
//Allow for anonymous modules
if (typeof name !== 'string') {
//Adjust args appropriately
callback = deps;
deps = name;
name = null;
}
//This module may not have dependencies
if (!isArray(deps)) {
callback = deps;
deps = null;
}
//If no name, and callback is a function, then figure out if it a
//CommonJS thing with dependencies.
if (!deps && isFunction(callback)) {
deps = [];
//Remove comments from the callback string,
//look for require calls, and pull them into the dependencies,
//but only if there are function args.
if (callback.length) {
callback
.toString()
.replace(commentRegExp, commentReplace)
.replace(cjsRequireRegExp, function (match, dep) {
deps.push(dep);
});
//May be a CommonJS thing even without require calls, but still
//could use exports, and module. Avoid doing exports and module
//work though if it just needs require.
//REQUIRES the function to expect the CommonJS variables in the
//order listed below.
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) {
node = currentlyAddingScript || getInteractiveScript();
if (node) {
if (!name) {
name = node.getAttribute('data-requiremodule');
}
context = contexts[node.getAttribute('data-requirecontext')];
}
}
//Always save off evaluating the def call until the script onload handler.
//This allows multiple modules to be in a file without prematurely
//tracing dependencies, and allows for anonymous module support,
//where the module name is not known until the script onload event
//occurs. If no context, use the global queue, and get it processed
//in the onscript load callback.
if (context) {
context.defQueue.push([name, deps, callback]);
context.defQueueMap[name] = true;
} else {
globalDefQueue.push([name, deps, callback]);
//console.log(globalDefQueue[0]);
}
};
它会起到如下几个作用:
- 1.支持commonjs规范,它会根据参数进行判断当前模块使用哪种规范,如果是commonjs规范就用正则将require里请求的js模块全部提取出来,作为当前模块的依赖。
- 2.把模块名,依赖和工厂函数一同加入全局或者当前上下文的队列中。
define在下载js文件后的第一时间执行,经过它的处理之后会进入到completeLoad函数。
completeLoad
代码:
completeLoad: function (moduleName) {
var found, args, mod,
shim = getOwn(config.shim, moduleName) || {},
shExports = shim.exports;
takeGlobalQueue();
while (defQueue.length) {
args = defQueue.shift();
if (args[0] === null) {
args[0] = moduleName;
//If already found an anonymous module and bound it
//to this name, then this is some other anon module
//waiting for its completeLoad to fire.
if (found) {
break;
}
found = true;
} else if (args[0] === moduleName) {
//Found matching define call for this script!
found = true;
}
callGetModule(args);
}
context.defQueueMap = {};
//Do this after the cycle of callGetModule in case the result
//of those calls/init calls changes the registry.
mod = getOwn(registry, moduleName);
if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
if (hasPathFallback(moduleName)) {
return;
} else {
return onError(makeError('nodefine',
'No define call for ' + moduleName,
null,
[moduleName]));
}
} else {
//A script that does not call define(), so just simulate
//the call for it.
callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
}
}
checkLoaded();
}
在completeLoad函数中会defQueue中的模块参数导出并调用callGetModule重新生成模块,原因应该是模块可能是有名字的,然后调用init函数初始化模块的依赖和工厂函数,并继续执行enable函数将其依赖转换为模块,然后调用check去fetch或者exec模块。最后把执行完的模块加入的define中保存,避免多次执行,并通知依赖它的模块执行defineMap,把它的执行值加入模块的依赖值数组中,并把依赖项减1,代码:
defineDep: function (i, depExports) {
//Because of cycles, defined callback for a given
//export can be called more than once.
if (!this.depMatched[i]) {
this.depMatched[i] = true;
this.depCount -= 1;
this.depExports[i] = depExports;
}
}
最后再度执行check函数,就这样层层递归并层层返回,直到所有模块执行完毕,这就是整个require的执行过程。
execCb
最后还要提到execCb这个函数,它是执行模块的函数,代码:
if ((this.events.error && this.map.isDefine) ||
req.onError !== defaultOnError) {
try {
exports = context.execCb(id, factory, depExports, exports);
} catch (e) {
err = e;
}
} else {
exports = context.execCb(id, factory, depExports, exports);
}
execCb: function (name, callback, args, exports) {
return callback.apply(exports, args);
}
在这里args为模块的depExports,callback是工厂函数,执行环境为exports,最后将其执行结果返回给exports。
commonJS
requireJs不仅支持amd规范,还同时支持commonJs规范,下面介绍下它是如何来支持commonJs的。
首先在前面提到过define的时候会将commonJs工厂函数中require过的所有模块都加入到deps中,然后在handlers中定义了工厂函数中的require,exports,module三个函数的实现。代码:
handlers = {
'require': function (mod) {
if (mod.require) {
return mod.require;
} else {
return (mod.require = context.makeRequire(mod.map));
}
},
'exports': function (mod) {
mod.usingExports = true;
if (mod.map.isDefine) {
if (mod.exports) {
return (defined[mod.map.id] = mod.exports);
} else {
return (mod.exports = defined[mod.map.id] = {});
}
}
},
'module': function (mod) {
if (mod.module) {
return mod.module;
} else {
return (mod.module = {
id: mod.map.id,
uri: mod.map.url,
config: function () {
return getOwn(config.config, mod.map.id) || {};
},
exports: mod.exports || (mod.exports = {})
});
}
}
}
从之前的分析中我们知道factory函数真正执行是在所有的依赖模块都已经defined之后,所以require的作用就是从definded的队列中直接取出模块,exports则是把其值加入defined队列,以暴露它自己,module则封装了模块id,exports等一系列模块,可以看作是mod的缩影,而mod就是requrire的模块本身。通过以上的方法,最终获得了对commonJs的支持。
结语
对于requireJs的分析到此告一段落,通过几天不断摸索,基本摸清了整个requireJs的工作流程,但其细节以及扩展功能和错误捕获等等,还无法完全理解,不得不承认自己离大师的境界还差的太远,同时还感到很多问题如果不是在实际中真正遇到,你是无法真正理解为什么它的代码会这么去写。所以在分析别人源码的时候,也不能过于深究,重点在摸清整体结构,从中掌握一些优秀技巧和设计模式,最终还是要自己去真正动手开发。