vscode 源码使用 vscode-loader 加载模块,vscode-loader 是异步模块定义 (AMD) 加载器的一种实现。而 AMD 规范的实现典范是 requirejs,可在浏览器、node 等环境中,异步加载 js 或模块。
本文先学习梳理 requirejs 的源码,了解 AMD 一般是如何实现的。在后面的文章中,再进一步学习 vscode-loader 的实现。
requirejs 的使用示例
require.js 在 浏览器中的使用方法如下:
html:<srcipt> 标签的 src 指定为 require.js,data-main 指定为入口 js。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<!-- 指定入口脚本 a/b.js -->
<script src="require.js" data-main="a/b.js"></script>
</head>
<body></body>
</html>
入口 js:调用 requirejs 方法,加载模块。
// a/b.js
// 配置模块路径
require.config({
paths: {
test: "test",
},
});
// 加载模块
requirejs(["test"], function (test) {
test.compare(2, 5);
});
定义模块:
// test.js
define("test", function () {
return {
compare: function (a, b) {
return a > b;
},
};
});
define 方法:define(id?, dependencies?, factory);
,第一个参数是模块名,第二个参数是依赖,第三个参数是模块的工厂函数,返回定义的模块。
requirejs 的代码解析
requirejs 的主体是一个自执行函数。下面代码省略了许多细节,先看一下基本的结构。
var requirejs, require, define;
(function (global, setTimeout) {
// 定义一系列的变量和函数,最主要是 newContext、req、define
function newContext(contextName) {}
req = requirejs = function (deps, callback, errback, optional) {}
define = function (name, deps, callback) {}
// 第一步:创建默认上下文
req({});
// 第二步:浏览器环境,查找入口 js,放到配置中
if (isBrowser && !cfg.skipDataMain) {
...
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
...
}
// 第三步:根据配置,加载入口 js
req(cfg);
}(this, (typeof setTimeout === 'undefined' ? undefined : setTimeout)));
可以看到,requirejs 可以分为三步:创建默认上下文、查找入口 js、加载入口 js。
第一步和第三步都是调用 req 函数,涉及代码较多。我们先看看相对简单的第二步,查找入口 js 的具体实现。
查找入口 js
从前面的示例 <script src="require.js" data-main="a/b.js" ></script>
,我们知道,入口 js 在 script 标签的 data-main 属性中指定。所以查找入口 js,也就是先找到 data-main,再进行解析。具体如下:
if (isBrowser && !cfg.skipDataMain) {
// 第一步:遍历 script 标签
// 注:eachReverse,对 scripts 数组,遍历执行传入的函数,函数返回值为 true 时中止遍历
eachReverse(scripts(), function (script) {
// 第二步:保存 script 标签的父元素
if (!head) {
head = script.parentNode;
}
// 第三步:获取 data-main 属性
dataMain = script.getAttribute('data-main');
// 第四步:解析 data-main,获取入口 js,放到配置中
if (dataMain) {
// 略,在下面展开讨论
...
}
});
}
// 相关函数如下:
/**
* 遍历数组,执行函数,函数返回 true 时,break
*/
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;
}
}
}
}
function scripts() {
return document.getElementsByTagName('script');
}
解析 data-main 的具体逻辑如下:
if (dataMain) {
mainScript = dataMain;
// 第一步:如果没有 baseUrl,则解析获取 baseUrl
// 注:mainScript.indexOf('!') === -1,该判断是指 data-main 值不是加载器插件模块的 ID。
if (!cfg.baseUrl && mainScript.indexOf("!") === -1) {
// 1. data-main 解析为 mainScript 和 subPath
/**
* 例子:
* dataMain = 'a', 解析出 mainScirpt = 'a', subPath = './'
* dataMain = 'a/b', 解析出 mainScript = 'b', subpath = 'a/'
*/
src = mainScript.split("/");
mainScript = src.pop();
subPath = src.length ? src.join("/") + "/" : "./";
// 2. 设置 cfg 的 baseUrl
cfg.baseUrl = subPath;
}
// 第二步:mainScript 去掉结尾的 .js
mainScript = mainScript.replace(jsSuffixRegExp, "");
// 第三步:如果 mainScript 仍然是路径,则回退到 dataMain
if (req.jsExtRegExp.test(mainScript)) {
mainScript = dataMain;
}
// 第四步:将 data-main 脚本放入 cfg.deps 中,等待后续加载
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
return true;
}
经过解析,得到配置对象 cfg。比如对于 data-main="./a/b.js"
,得到的 cfg 如下:
cfg = {
baseUrl: "./a/",
deps: ["b"],
};
req 加载入口 js
<!-- 接下来,我们继续看第一步、第三步操作:调用 req 函数,创建默认上下文、加载入口 js -->
配置好入口文件后,下一步就是调用 req 函数,加载入口 js。第一步操作,创建默认上下文也是调用 req 函数,我们放在一起看。
defContextName = "_";
req = requirejs = function (deps, callback, errback, optional) {
// 第一步:contextName 设置为默认值
var context,
config,
contextName = defContextName;
// 第二步:确定第一个参数,是否为 config 对象。如果是,重新修改其他参数。
if (!isArray(deps) && typeof deps !== "string") {
// deps is a config object
config = deps;
if (isArray(callback)) {
// Adjust args if there are dependencies
deps = callback;
callback = errback;
errback = optional;
} else {
deps = [];
}
}
// 第三步:根据 config 对象,更新 contextName
if (config && config.context) {
contextName = config.context;
}
// 第四步:根据 contextName 获取或新建上下文
context = getOwn(contexts, contextName);
if (!context) {
context = contexts[contextName] = req.s.newContext(contextName);
}
// 第五步:如果有 config 对象,就更新上下文的配置
if (config) {
context.configure(config);
}
// 第六步:调用上下文的 require 方法
return context.require(deps, callback, errback);
};
s = req.s = {
contexts: contexts,
newContext: newContext,
};
可以看到 req
函数就是 requirejs
函数。它有两种调用方式,一种是不传入 config 对象,一种是传入。
- 不传入 config 对象:
- 直接使用默认上下文(获取或新建默认上下文),调用默认上下文的
require
方法。
- 传入 config 对象:
- 根据 config 更新 contextName,根据 contextName 获取或新建上下文(注意:如果它没有更新,也还是默认上下文)。
- 用 config 更新上下文的配置,再调用上下文的
require
方法。
<!-- todo -->
<!-- 另外,如果只传入 config 对象,不传入 deps, callback, errback 等参数,那么只会设置上下文配置,而第六步的 require 其实是不生效的。 -->
req 执行过程
req({})
,传了一个没有属性的对象进去,创建了默认上下文。
req(cfg)
,还是以前面 data-main="./a/b.js"
为例,传入参数为 { baseUrl: './a/', deps: ['b'] },加载了入口文件。
req 前三步的执行逻辑如下:
req = requirejs = function (deps, callback, errback, optional) {
// 第一步:contextName = '_'
var context, config,
contextName = defContextName;
// 第二步:
// req({}),第一个参数是 {},结果: config = {}, deps = []。
// req(cfg),第一个参数是 { baseUrl: './a/', deps: ['b'] },结果:config = { baseUrl: './a/', deps: ['b'] }, deps = []
if (!isArray(deps) && typeof deps !== 'string') {
// config = 第一个参数
config = deps;
// 未传入第二个参数,deps = []
if (isArray(callback)) {
deps = callback;
callback = errback;
errback = optional;
} else {
deps = [];
}
}
// 第三步:
// req({}),config 为 {},所以 contextName 依然为默认值 '_'
// req(cfg),config 为 { baseUrl: './a/', deps: ['b'] }, 所以 contextName 依然为默认值 '_'
if (config && config.context) {
contextName = config.context;
}
...
};
我们继续看 req 函数的第四步。
req({})
,一开始还没有默认上下文,所以会新建默认上下文。
req(cfg)
的 contextName 也是默认值 '_',而默认上下文已经新建,所以会直接获取默认上下文。
// req 函数的第四步:
// req({}),contextName 为默认值 '_',一开始没有默认上下文,所以走 req-4.2,新建默认上下文。
// req(cfg),contextName 为默认值 '_',已经有默认上下文,所以走 req-4.1,获取默认上下文。
req = requirejs = function (deps, callback, errback, optional) {
...
// req-4.1. 根据 contextName 获取 context。
context = getOwn(contexts, contextName);
// req-4.2. 如果没有 context,根据 contextName 新建一个上下文。
if (!context) {
context = contexts[contextName] = req.s.newContext(contextName);
}
...
}
/* req-4.1 获取上下文 */
// getOwn 的第一个参数 contexts,初始化时是 {}
contexts = {},
// getOwn:判断对象中,是否有某个属性,如果有就返回属性值
function getOwn(obj, prop) {
return hasProp(obj, prop) && obj[prop];
}
// hasProp:判断对象中,是否有某个属性
function hasProp(obj, prop) {
return hasOwn.call(obj, prop);
}
// hasOwn
op = Object.prototype,
hasOwn = op.hasOwnProperty,
继续看 req-4.2 新建上下文的逻辑,由于代码量大,这里进行省略简化。
/* req-4.2 新建上下文 */
function newContext(contextName) {
// 定义一堆变量和函数,这里只看 context
var context
...
// req-4.2.1:context 赋值
context = {
config: config,
contextName: contextName,
...
// 为上下文设置配置
configure: function (cfg) {
// 确保 baseUrl 以 / 结束
if (cfg.baseUrl) {
if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
cfg.baseUrl += '/';
}
}
...
// configure 的最后一步:如果指定了 deps 或 callback,则使用这些参数调用 require。
if (cfg.deps || cfg.callback) {
context.require(cfg.deps || [], cfg.callback);
}
},
// 加载模块
makeRequire: function (relMap, options) { ... },
...
}
// req-4.2.2:设置 context.require,并返回 context
context.require = context.makeRequire();
return context;
}
可以看到新建上下文,主要是对 context 进行赋值,定义一系列的属性和方法,并返回 context。
回到 req 函数,第五步是设置 config,如果指定了 deps,则调用 require 加载模块。
第六步是返回 context.require,加载模块。
req = requirejs = function (deps, callback, errback, optional) {
...
// 第五步:调用 context.configure 更新配置
/**
* req({}),config 为 {},结果:context.config = {config: baseUrl: "./", bundles: {}, config: {}, paths: {}, pkgs: {}, shim: {}, waitSeconds: 7}
* req(cfg),config 为 { baseUrl: './a/', deps: ['b'] },结果:context.config = {config: baseUrl: "./a/", deps: ["b"], bundles: {}, config: {}, paths: {}, pkgs: {}, shim: {}, waitSeconds: 7}, 由于 cfg.deps 为 ['b'],在 configure 的最后一步会调用 context.require(cfg.deps),加载入口 js
*/
if (config) {
context.configure(config);
}
// 第六步:context.require,从第二步可知,deps 均为 [],所以这里没有依赖模块加载。
return context.require(deps, callback, errback);
};
加载模块
梳理完 req 函数的执行过程,可以看到,req(cfg)
在第五步会通过 context.require(cfg.deps)
,加载入口 js,其中 cfg.deps
为 ['b']。
接下来,就继续查看加载模块的实现逻辑。在前面(req-4.2.2),可以看到 context.require = context.makeRequire()
。
context = {
...
makeRequire: function (relMap, options) {
options = options || {};
// 第一步:定义 localRequire(require 的实现)
function localRequire(deps, callback, errback) {
var id, map, requireMod;
if (options.enableBuildCallback && callback && isFunction(callback)) {
callback.__requireJsBuild = true;
}
// require-1. 如果 deps 是字符串类型,根据模块名称获取模块id ,再返回 defined[id]
if (typeof deps === 'string') {
...
return defined[id];
}
// require-2. 抓取全局队列中等待的 defines
intakeDefines();
// require-3. 在 nextTick 中,加载所有依赖
context.nextTick(function () {
intakeDefines();
// 获取模块加载器(重点)
requireMod = getModule(makeModuleMap(null, relMap));
requireMod.skipMap = options.skipMap;
// 初始化模块(重点)
requireMod.init(deps, callback, errback, {
enabled: true
});
checkLoaded();
});
return localRequire;
}
// 第二步:localReuire 增加 isBrowser, toUrl, defined, specified 四个方法
mixin(localRequire, {
isBrowser: isBrowser,
toUrl: function (moduleNamePlusExt) { ... }, // module name + .extension 转为 url 路径
defined: function (id) { ... },
specified: function (id) { ... }
});
// 第三步:localRequire 增加 undef 方法,注意:只允许在顶级 require 调用 undef
if (!relMap) {
localRequire.undef = function (id) {
...
};
}
// 第四步:返回 localRequire
return localRequire;
}
}
context.require = context.makeRequire()
context.makeRequire()
返回的是 localRequire
,而 localRequire
使用 context.nextTick
,在未来的事件循环中加载依赖。
nextTick
的实现如下:
context = {
nextTick: req.nextTick,
};
/**
* 在当前任务结束之后执行某些操作,具体来说是通过 setTimeout 将操作放到事件循环的消息队列中,排队等待执行。
* 其他环境下,如果有比 setTimeout 更好的解决方案,就会改写该方法。
* 注:延时 4ms,是因为 setTimeout 的最小延时时间为 4ms。
*/
req.nextTick =
typeof setTimeout !== "undefined"
? function (fn) {
setTimeout(fn, 4);
}
: function (fn) {
fn();
};
继续看,nextTick
中加载模块的逻辑:
context.nextTick(function () {
...
// module-1. 获取模块加载器
requireMod = getModule(makeModuleMap(null, relMap));
// module-2. 初始化模块(重点)
requireMod.init(deps, callback, errback, {
enabled: true
});
...
});
// module-1. 获取或新建模块加载器:new context.Module(depMap)
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.map = map;
...
};
Module.prototype = {
...
};
context = {
...
Module: Module
}
这里通过 getModule
返回 Module
的实例,Module
包含模块相关的属性和方法。
接下来,调用 Module
的 init
方法初始化模块:
// module-2. 初始化模块
Module.prototype = {
init: function (depMaps, factory, errback, options) {
...
// 如果未启动,就启动该模块。如果已启动,检查并启动其依赖项。
if (options.enabled || this.enabled) {
this.enable();
} else {
this.check();
}
},
enable: function () {
...
// module-2.1. 遍历启动依赖
each(this.depMaps, bind(this, function (depMap, i) {
...
if (!hasProp(handlers, id) && mod && !mod.enabled) {
// 调用 context.enable 启动
context.enable(depMap, this);
}
}));
// module-2.2. 加载当前模块
this.check();
}
}
context = {
...
enable: function (depMap) {
// 如果模块仍在注册表中等待启动,则启动该模块。
var mod = getOwn(registry, depMap.id);
if (mod) {
// 递归加载依赖项,getModule,并 enable
getModule(depMap).enable();
}
},
}
module-2.1 的执行流程:this.init()
-> this.enale()
-> context.enable(depMap, this)
-> getModule(depMap).enable()
-> ...
在当前模块执行 enable()
时,遍历启动依赖模块;依赖模块执行 enable()
时,又会遍历启动它里面的依赖。就这样,通过递归,启动所有依赖模块。
继续看 module-2.2 this.check()
加载当前模块。
Module.prototype = {
check: function () {
...
if (!this.inited) {
// 调用 this.fetch
if (!hasProp(context.defQueueMap, id)) {
this.fetch();
}
}
...
},
fetch: function () {
...
// 调用 this.load。this.callPlugin 是加载插件的,最终也会调用 this.load()。
return map.prefix ? this.callPlugin() : this.load();
},
load: function () {
var url = this.map.url;
if (!urlFetched[url]) {
urlFetched[url] = true;
// 调用 context.load
context.load(this.map.id, url);
}
}
}
context = {
...
// 执行 req.load
load: function (id, url) {
req.load(context, id, url);
}
}
req.load = function (context, moduleName, url) {
var config = (context && context.config) || {},
node;
if (isBrowser) {
// 浏览器环境,新建 script 标签
node = req.createNode(config, moduleName, url);
// 记录上下文名和模块名
node.setAttribute('data-requirecontext', context.contextName);
node.setAttribute('data-requiremodule', moduleName);
// 监听事件
if (node.attachEvent &&
!(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
!isOpera) {
// 兼容 IE
useInteractive = true;
node.attachEvent('onreadystatechange', context.onScriptLoad);
} else {
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
}
// 设置 src
node.src = url;
if (config.onNodeCreated) {
config.onNodeCreated(node, config, moduleName, url);
}
// 插入 scirpt 标签
currentlyAddingScript = node;
if (baseElement) {
head.insertBefore(node, baseElement);
} else {
head.appendChild(node);
}
currentlyAddingScript = null;
return node;
} else if (isWebWorker) {
...
}
};
// 新建 script 节点
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;
return node;
};
module-2.2 加载模块的执行流程:this.check()
-> this.fetch()
-> this.load()
-> context.load()
-> req.load()
-> req.createNode()
。
可以看到,在浏览器环境,是通过插入 <script>
标签,设置 node.src = url
来加载模块的。
define 定义模块
在 requirejs 的使用示例中,有一个 test 模块。在调用 require
函数时,会通过 context.require(deps, callback, errback)
,加载 test.js。
// a/b.js
// 加载模块
requirejs(["test"], function (test) {
test.compare(2, 5);
});
而 test.js 中,通过 define
函数定义了 test 模块。
// test.js
define("test", function () {
return {
compare: function (a, b) {
return a > b;
},
};
});
下面看一下 define
函数的实现:
(commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/gm),
(cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g),
(define = function (name, deps, callback) {
var node, context;
// 第一步:没有传入 name,调整参数
// 示例中的 test 模块,name 为 'test'
if (typeof name !== "string") {
callback = deps;
deps = name;
name = null;
}
// 第二步:没有传入 deps,调整参数
// test 模块,没有传入 deps,callback = fn,deps = null
if (!isArray(deps)) {
callback = deps;
deps = null;
}
// 第三步:没有传入 deps, callback 为函数,从 callback 中获取依赖
// test 模块,执行这部分代码,由于 callback 没有形式参数,结果:deps = []
if (!deps && isFunction(callback)) {
deps = [];
if (callback.length) {
callback
.toString()
.replace(commentRegExp, commentReplace) // 删除注释
.replace(cjsRequireRegExp, function (match, dep) {
// 获取依赖模块
deps.push(dep);
});
// Function.length 对应函数形参的个数,只有一个参数时,deps = ['require'];否则 deps = ['require', 'exports', 'module', ...deps]
deps = (callback.length === 1
? ["require"]
: ["require", "exports", "module"]
).concat(deps);
}
}
// 第四步:兼容 IE 6-8
// test 模块,非 IE,不执行这部分代码
if (useInteractive) {
node = currentlyAddingScript || getInteractiveScript();
if (node) {
if (!name) {
name = node.getAttribute("data-requiremodule");
}
context = contexts[node.getAttribute("data-requirecontext")];
}
}
/**
* 第五步:如果有上下文,则将依赖加入 context.defQueue。如果没有上下文,则将依赖加入全局队列。
* 脚本 onload 时,再调用 def。这允许一个文件有多个模块,而不会过早地跟踪依赖,并支持匿名模块,其中模块名称在脚本 onload 事件发生之前是未知的。
*/
// test 模块,由于没有执行第四步,没有获取 context,所以加入 globalDefQueue。
if (context) {
context.defQueue.push([name, deps, callback]);
context.defQueueMap[name] = true;
} else {
globalDefQueue.push([name, deps, callback]);
}
});
通过 define('test', function() {...})
,依赖模块加入 context.defQueue
或 globalDefQueue
中,在脚本 onload
之后加载依赖模块。
回顾前面加载模块的流程,module-2.2 this.check()
-> this.fetch()
-> this.load()
-> context.load()
-> req.load()
。在 req.load
中,会监听 test 脚本的 load
事件,并在这里再次初始化 test 模块。具体如下:
req.load = function (context, moduleName, url) {
...
// 第一步:监听 script 的 load 事件,调用 context.onScriptLoad
node.addEventListener('load', context.onScriptLoad, false);
...
};
function newContext(contextName) {
var defQueue = []
context = {
...
defQueue: defQueue,
onScriptLoad: function (evt) {
if (evt.type === 'load' ||
(readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
interactiveScript = null;
// 第二步:获取模块名,调用 completeLoad
var data = getScriptData(evt);
context.completeLoad(data.id);
}
},
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 (found) {
break;
}
found = true;
} else if (args[0] === moduleName) {
found = true;
}
// 第四步:获取模块并初始化
callGetModule(args);
}
context.defQueueMap = {};
...
},
}
function takeGlobalQueue() {
// 将 globalDefQueue 中的依赖放入 context 的 defQueue
if (globalDefQueue.length) {
each(globalDefQueue, function(queueItem) {
var id = queueItem[0];
if (typeof id === 'string') {
context.defQueueMap[id] = true;
}
defQueue.push(queueItem);
});
globalDefQueue = [];
}
}
function callGetModule(args) {
// 跳过已定义的模块
if (!hasProp(defined, args[0])) {
// 获取模块,初始化
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
}
}
}
可以看到,依然是通过 getModule
获取模块,再通过 init
初始化。初始化流程和前面 req 函数一样:this.init()
-> this.enale()
-> 遍历启动依赖模块(test 模块没有依赖,跳过) -> this.check()
,不再赘述。不同的是,this.check()
中的流程(执行 test 模块的工厂函数,并赋值给 this.exports
和 defined['test']
):
Module.prototype = {
check: function () {
...
// 已经初始化过,这次不走 this.fetch()
if (!this.inited) {
if (!hasProp(context.defQueueMap, id)) {
this.fetch();
}
} else if (this.error) {
this.emit('error', this.error);
} else if (!this.defining) {
// 走这个流程
this.defining = true;
if (this.depCount < 1 && !this.defined) {
if (isFunction(factory)) {
// 第一步:调用 context.execCb,执行 factory 函数,执行结果赋值给 exports
// 对于 test 模块,入参 id = "test", factory = test 模块的工厂函数, depExports = [], exports = undefined, 结果:exports = { compare: fn }
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);
}
...
} else {
exports = factory;
}
this.exports = exports;
if (this.map.isDefine && !this.ignore) {
// 第二步:设置 defined[id]
// 对于 test 模块,id = 'test', exports = { compare: fn },结果: defined['test'] = { compare: fn }
defined[id] = exports;
if (req.onResourceLoad) {
var resLoadMaps = [];
each(this.depMaps, function (depMap) {
resLoadMaps.push(depMap.normalizedMap || depMap);
});
req.onResourceLoad(context, this.map, resLoadMaps);
}
}
cleanRegistry(id);
this.defined = true;
}
this.defining = false;
if (this.defined && !this.defineEmitted) {
this.defineEmitted = true;
// 第三步:触发 defined 事件
this.emit('defined', this.exports);
this.defineEmitComplete = true;
}
}
},
// 注意:这里 enable 的模块对应 require(['test'], f(test)),在该模块 enable 时遍历其依赖 ['test'],并监听依赖的 defined 事件
enable: function () {
...
each(this.depMaps, bind(this, function (depMap, i) {
var id, mod, handler;
if (typeof depMap === 'string') {
...
this.depCount += 1;
// 第四步:监听 defined 事件
// test 模块,depExports = { compare: fn }
on(depMap, 'defined', bind(this, function (depExports) {
if (this.undefed) {
return;
}
// 第五步:将 exports 存放到 this.depExports 数组中
this.defineDep(i, depExports);
// 第六步:再次执行 this.check()
this.check();
}));
...
}
...
}
...
},
defineDep: function (i, depExports) {
if (!this.depMatched[i]) {
this.depMatched[i] = true;
this.depCount -= 1;
// 将 exports 存放到 this.depExports 数组中
this.depExports[i] = depExports;
}
},
}
function newContext(contextName) {
context = {
execCb: function (name, callback, args, exports) {
return callback.apply(exports, args);
},
...
}
}
这里,执行 define('test', function() {...})
中工厂函数,得到 test 模块的对象 { compare: fn }
。再将模块对象赋值给 this.exports
,用于输出模块对象。同时保存到上下文的 defined
对象中,缓存起来。(第一步 - 第二步)
接着,触发 defined
事件,将 exports
存放到 this.depExports
数组中,并执行 require(['test'], f(test))
模块的 this.check()
。(第三步 - 第六步)
继续看这一次的 this.check()
流程:
Module.prototype = {
check: function () {
...
// factory = require 中的函数 f(test), depExports = [compare], exports = undefined
// 调用 context.execCb,执行 require(['test'], function (test) {}) 中的函数,并将 depExports(即 test 模块的返回值,compare 函数) 作为参数传入。
exports = context.execCb(id, factory, depExports, exports);
...
}
}
}
这次调用 check
,执行的是 require
中传入的函数,并将 this.depExports
作为参数传入,也就是前面 test 模块的返回值 { compare: fn }
。至此,test 模块加载完成。