原生js手动实现异步模块加载,整体设计如下:
(function(exports) {
var moduleCache = {}, // 模块缓存
_loadModule = function() {}, // 加载模块,私有方法不需知道实现细节
_setModule = function() {}, // 执行模块,私有方法不需知道实现细节
_module = function() {}, // 定义模块
var Amd = {
module: module // 特权方法,暴露出模块定义接口但隐藏实现细节
};
exports.Amd = Amd;
})(window)
加载对象用常见的IIFE(立即执行函数表达式)封装起来,隐藏实现细节,防止命名冲突,IIFE中传入window对象,在全局对象中注册该加载对象。
加载对象对于后续的使用者来说,只需暴露出module方法,因为使用者不需要知道他引用的模块是如何加载及执行的,能用即可。
下面详细说下加载对象的_module、_loadModule、_setModule的实现细节:
对于module方法,它接受3个或3个以下的参数。首先,从功能上设计,它可以定义一个模块;其次它还可以执行引用的模块;最后为了容错,对于直接传入的函数,它应该立即执行。
_module = function() {
var args = [].slice.call(arguments), // 将实参由类数组转为数组
callback = args.pop(), // 回调函数始终放在参数末尾
deps = args.length && Object.toString.call(args[args.length - 1]) === ‘[object Array]’ && args.pop() || [], // 取出回调函数后,若还有参数并且最后一个参数类型是数组,则为引用的模块
src = args.length > 0 ? args.pop() : null, // 取出引用模块后,若还有参数,则为模块名称,即模块路径,否则为模块调用
unloadModule = 0, // 记录各模块引用的模块未加载完毕的数目
params = [], // 顺序放置各模块引用的模块的真实定义,即该引用模块的实际对象,此为各模块执行自身回调时的参数数组
i = -1, // 记录各模块引用的模块的引用顺序
name, // 引用模块的名称
len; // 判断引用模块的数目
if(len = deps.length) { // 先赋值再判断,若引用模块数目为0,则隐式转换为布尔值为false,否则为true。注:js中可隐式转为false的有6个falsy值:””, 正负0 , NaN, false, null, undefined。除此之外,发生隐式转换时,均为true
// 若有引用模块,先加载各引用的模块,循环加载最先引用的模块
while(name = deps.shift()) {
// 此处的IIFE形成的闭包是必要的,因为各引用模块加载完成的先手顺序是未知,不用闭包记录每个引用模块被引用的顺序,会导致后续执行模块的回调方法时,传入模块的实际参数与形参无法对应。此处的IIFE会形成一个独立的函数作用域,该作用域中记录了while循环时传入此IIFE的引用模块的索引值,后续该引用模块加载完毕后执行_loadModule中的回调方法时会在此函数作用域中找到它索引值。
(function(i) {
// 每加载一个引用模块,该模块的未加载完毕的引用模块数目加1
unloadModule++;
_loadModule(src, function(objModule) {
// 加载完毕的引用模块会执行此回调,并传回自身的定义即此引用模块的实际对象,此时将引用此引用模块的模块的未加载完毕的引用模块数目减1,并将该引用模块的实际对象按被引用的顺序加入到params中,用作参数数组,待所有引用模块加载完毕后执行模块回调时传入模块回调函数用作参数
unloadModule--;
params[i] = objModule;
if(unloadModule === 0) {
// 所有引用模块均已加载完毕,则执行模块回调方法
_setModule(src, params, callback);
}
});
})(++i);
}
} else {
// 未引用其他模块,则直接执行此模块回调
_setModule(src, [], callback);
}
}
对于加载模块方法_loadModule,它应接收两个参数:1.模块名称2.模块加载完毕后将该模块的实际对象加入到引用此模块的其他模块的参数数组中
var _loadModule = function(name, callback) {
// 先看模块缓存中是否已记录该模块,若存在,直接读缓存
if(moduleCache[name]) {
var _module = moduleCache[name];
if(_module.status === ‘loaded’) {
// 若该模块已加载完毕,则立即执行回调
setTimeOut(callback(_module.export), 0);
} else {
// 若该模块正在加载,则在此模块的待执行回调队列中加入此回调
_module.queue.push(callback);
}
} else {
moduleCache[name] = {
name: name,
status: ‘loading’, // 新加载的模块状态设为loading,因为此时还未新建script标签,动态向document.head中新增此模块,此时的模块的状态必定时正在加载
export: null, // 该模块是实际定义即该模块的实际对象
queue: [callback] // 待执行回调数组,将所有引用此模块的模块回调加入到此数组,待此模块加载完毕,再顺序执行此数组中的回调,将该模块实际对象顺序加入引用此模块的其他模块的回调参数数组中
};
// 加载模块,模块机制其实就是获取到该模块所在的文件路径,在document.head中新增script标签,标签引用的路径即此模块路径。所以所有模块的定义名称都严格按照其所在路径定义,如:一个模块文件所在路径为’YSTMusic/views/Aboli.js’,该模块的定义应为:module(‘YSTMusic/views/Aboli’, dependences, callback),不然会因script中src引用地址不正确报错。
loadScript(name);
}
}
var loadScript = function(name) {
name = name.toString().replace(‘YSTMusic.’, ‘’) + ‘.js’;
var _script = document.createElement(‘script’);
_script.src = name;
_script.async = true;
_script.type = ‘text/javascript’;
document.head.appendChild(_script);
}
执行模块方法_setModule,应接收3个参数,模块名称,模块引用数组,执行函数。另外,js中对于文件的加载是异步的,所以各模块的定义文件通过document.head加入,后续文件加载完成后,必定会执行文件中的module方法,也必定会执行到_setModule方法。
var _setModule = function(name, deps, callback) {
if(name) {
// 若是模块定义,此时模块所在的js文件已加载完毕,该模块的状态应设为loaded,该模块的export应更新为此模块的实际对象,并顺序执行此模块的待执行回调数组queue中的回调方法
var _module = moduleCache[name],
fn;
_module.status = ‘loaded’;
_module.export = callback && callback.apply(_module, deps);
while(fn = _module.shife()) {
fn(_module.export);
}
} else {
// 若是单纯的执行模块,例如: module([‘a’, ‘b’], function(a, b) {});
callback.apply(null, deps);
}
}