1.前言
前端模块化的框架都有自己的模块化规范。今天,我来介绍下早期传统经典的amd规范实现细节。
2.什么是amd规范
amd全称Asynchronous Module Definition(异步模块定义),由于Web网站的业务逐渐复杂,所以需要引入模块化的理念去处理业务。而如何去维护管理、加载这些模块呢?当我们在执行模块脚本的时候,传统的脚本通过同步的加载方式会阻塞脚本之后的HTML渲染,或者,一个页面上需要引入多个脚本文件,则需要写入很多次的脚本引入代码,而amd则是解决这一问题所诞生的规范。
3.Require.js的介绍
require.js是amd规范的实现,本文中的源代码版本为:RequireJS 2.3.6。下面,让我们分析学习下,它是如何实现amd规范的。
3.1 异步加载方式的处理
首先,要处理异步问题,就应该先考虑哪些问题会存在同步的情况:
1.require.js脚本自身的加载
2.require.js依赖模块的加载
3.1.1 require.js脚本自身的异步加载
req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
setTimeout(fn, 4);
} : function (fn) {
fn();
};
context = {
nextTick: req.nextTick
}
context.nextTick(function () {
console.log('tick')
//Some defines could have been added since the
//require call, collect them.
intakeDefines();
requireMod = getModule(makeModuleMap(null, relMap));
//Store if map config should be applied to this require
//call for dependencies.
requireMod.skipMap = options.skipMap;
requireMod.init(deps, callback, errback, {
enabled: true
});
checkLoaded();
});
require.js利用了setTimeout定时器的执行优先级比JS主线程优先级低的特点,解决了自身脚本的异步加载问题。
3.1.2 依赖模块异步加载
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;
};
node = req.createNode(config, moduleName, url);
node.setAttribute('data-requirecontext', context.contextName);
node.setAttribute('data-requiremodule', moduleName);
node.src = url;
require.js会把依赖的模块通过动态创建的脚本节点,并且设置脚本为async异步加载方式,然后再根据情况,动态的追加的html文件的头部或者其他地方。
3.2 Require.js的生命周期

上图是我整理的require.js的大致流程图,从引入require.js开始,如下所示:
<script type="text/javascript" data-main="./a" src="require.js"></script>
require.js通过读取data-main标签的属性,来确定入口脚本信息cfg。然后再执行req(cfg)进行引入。在这之前,会有一次req({})初始化的工作,具体实现如下:
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.nextTick定时器来加载模块,模块的加载方式,我刚开始的时候已经介绍了。define定义的依赖,最终会入栈到依赖队列中,当加载完最后一个脚本文件时,先执行主脚本,然后按序回溯调用。
4.简易版require.js的实现
(function(global){
var context = {
exec: [],
current_id: undefined,
define: function(deps, callback) {
context.localRequire(deps, callback);
},
require: function(deps, callback) {
context.localRequire(deps, callback);
},
localRequire: function(deps, callback) {
let mod = context.makeModule(deps);
context.exec.push({id:context.current_id, fn: callback});
if (-1 === context.exec.findIndex((item)=>mod.id === item.id)) {
context.createNode(mod);
}
},
makeRequire: function() {
let main = document.getElementsByTagName('script')[0].getAttribute('data-main');
let module = this.makeModule(main);
this.current_id = module.id;
this.createNode(module);
},
createNode: function(deps) {
var node = document.createElement('script');
node.async = true;
node.src = deps.src;
node.setAttribute('module_id', deps.id);
var head = document.getElementsByTagName('head')[0];
head.appendChild(node);
node.addEventListener('load', function(event){
if (event.type === 'load') {
let module_id = this.getAttribute('module_id');
context.current_id = module_id;
context.execMod();
}
});
},
makeModule: function(deps) {
return {
id: deps.substring(deps.indexOf('/') + 1),
src: deps + '.js'
}
},
execMod: function() {
while(this.exec.length) {
let script = this.exec.pop();
setTimeout(() => {
script && script.fn();
}, 4)
}
}
}
global.define = context.define;
global.require = context.require;
setTimeout(()=> context.makeRequire(), 4);
})(window)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="text/javascript" data-main="./b" src="require.js"></script>
</head>
<body>
<div></div>
</body>
</html>
define('c', function(){
console.log('b')
})
require('d', function(){
console.log('c')
})
//循环引用c
require('c', function(){
console.log('d')
})
console.log('d2')
