目录
前言
接上一篇,上一篇加载的入口为app模块,当app模块下载完成之后,就会获得执行。
app的依赖加载流程
贴上app的代码:
define(
function(require) {
'use strict';
var moduleA = require('./lib/moduleA');
console.log("I am app! calling moduleA.hello!");
moduleA.helloA();
});
当app加载完成,就会执行define这个函数,将'./lib/moduleA'这个模块放入到app的deps中。
最后,会将[name,deps,callback]这个三元数组作为globalDefQueue的元素扔进去。
globalDefQueue就是数据中转中心,后续的真正处理逻辑会把这个三原数组拿到context中。
define执行完之后,页面产生load事件,由于之前在req.load中生成app的script标签的时候,就绑定了load事件处理函数,这个时候该事件处理函数获得执行。
这个事件处理函数还是很巧妙的,也是实现模块的依赖项加载的关键地方。
onScriptLoad: function (evt) {
//Using currentTarget instead of target for Firefox 2.0's sake. Not
//all old browsers will be supported, but this one was easy enough
//to support and still makes sense.
if (evt.type === 'load' ||
(readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
//Reset interactive script so a script node is not held onto for
//to long.
interactiveScript = null;
//Pull out the name of the module and the context.
var data = getScriptData(evt);
context.completeLoad(data.id);
}
},
onScriptLoad实际上最后调用了context.completeLoad。
来看context.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这个函数首先调用takeGlobalQueue把globalDefQueue中的元素拿到DefQueue中,然后再通过while循环从DefQueue中拿元素出来,调用callGetModule进入依赖项加载逻辑。
function callGetModule(args) {
//Skip modules already defined.
if (!hasProp(defined, args[0])) {
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
}
}
在callGetModule中,会获取到module对象,然后调用module对象的init方法,于是就开始了加载的初始化过程,接下来就与之前app的加载一致了。
onScriptLoad的执行过程中,通过getScriptData获取到了当前Script标签对应的模块id,然后使用模块id作为参数调用context.completeLoad。
这个模块id的获取原理:
在一开始创建script标签的时候,就通过data-*属性将module name放入到script中了,现在的获取过程,其实就是从script标签中获取对应的data-*属性而已。
这个也是秒啊,通过data-*属性来暂存一些数据,比用全局变量舒服多了,但是效率肯定不如全局变量了。
继续执行,takeGlobalQueue把globalDefQueue中的数据全部拿到了context的defQueue中,然后依次从defQueue中拿出元素(这个元素是[name,deps,callback]三元数组),再通过callGetModule调用加载deps中的依赖项。
继续执行,进入callGetModule中,这里的args[0]为app,所以这里getModule返回的module就是app这个module,然后通过(app).init将deps与callback与(app)这个module关联起来,后面通过(app).enable完成deps的加载,这与上一篇的(_@r5).enable的加载就基本一致了。
最后进入到(app).enable中执行,完成依赖模块加载。
接下来的过程,与上一篇中基本一致。
这是一个递归的过程。
app执行完define,转到load事件处理函数中,初始化一个app module对象:(app)。
然后(app).init继续加载app依赖的模块(本例中为lib/moduleA)。
moduleA下载完之后,执行define,转到load事件处理函数中,初始化一个moduleA module对象:(moduleA)。
然后(moduleA).init继续执行,由于此时moduleA已经没有依赖模块了,所以这里会转向别的执行流程,这个执行流程就是下一篇中要说到的requirejs的模块执行顺序性的核心原理-------订阅-发布设计模式。
总结:
1、通过load事件处理函数onScriptLoad获取到当前加载完成的script对应的模块名,本例为app。
2、通过completeLoad中调用callGetModule进入到(app).init中。
3、在(app).init中通过(app).enable完成了对应的deps的加载。
4、此后便是一个递归的过程,直到没有依赖需要加载,递归触底返回,转向开始执行define中的回调函数。
后续
define中的callback的执行的顺序性是如何保证的?它们是怎么获得执行的?
简单的来说就是发布-订阅模式,requirejs的module对象内部实现了两个properties:emit和on,正好对应发布-订阅模式。每一个模块都会订阅其依赖模块的defined事件(事件回调函数就是相应的define中的callback),然后当一个模块defined之后,就会在this.check中通过emit发出'defined'事件,于是该模块的订阅者的回调获得执行。