简介
众所周知,模块化开是会将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控、可维护、可拓展,提高模块复用率。而在js中,异步模块模式的情况则比较多,所谓异步模块模式,是在请求发出后,继续其他业务逻辑,直到模块加载完成后执行后续的逻辑,实现模块开发中对模块加载完成后的引用。
今天就来分析一下异步加载模块,本文通过创建与调度模块、加载模块和设置模块三个方面来分析
创建与调度模块
创建与调度方法集模块创建方法于一身。在这个方法中腰遍历所有依赖模块,并判断所有模块都存在才可执行回调函数,否则加载相应文件,直到文件加载完成才执行回调函数。
实现代码如下:
/** * 创建或调用模块方法 * @param url 模块url * @param modDeps 依赖模块 * @param modCallback 模块主函数 */ F.module = function(url, modDeps, modCallback) { // 将参数转化为数组 let args = [].slice.call(arguments); // 获取模块构造函数(参数数组中最后一个参数成员) let callback = args.pop(); // 获取依赖模块(紧邻回调函数参数,且数据类型为数组) let deps = (args.length && args[args.length - 1] instanceof Array) ? args.pop() : []; // 该模块url(模块ID) url = args.length ? args.pop() : null; // 依赖模块序列 let params = []; // 未加载的依赖模块数量统计 let depsCount = 0; // 依赖模块序列中索引值 let i = 0; // 依赖模块序列长度 let len; if(len = deps.length) { while(i < len) { (function(i) { // 增加未加载依赖模块数量统计 depsCount++; // 异步加载依赖模块 loadModule(deps[i], function(mod) { // 依赖模块序列中添加依赖模块数量统一减一 depsCount--; params[i] = mod; // 如果依赖模块全部加载 if(depsCount === 0) { // 在模块缓存器中矫正该模块,并执行构造函数 setModule(url, params, callback); } }); })(i); // 遍历下一个模块 i++; } // 无依赖模块,直接执行回调函数 } else { // 在模块缓存器中矫正该模块,并执行构造函数 setModule(url, [], callback); } }
在module方法中有两个方法还没有定义:loadModule(加载模块)和setModule(设置模块),接下来,便一一介绍
加载模块
loadModule方法目的是加载依赖模块对应的文件并执行回调函数。对此可分三种情况处理:
- 如果文件已经被要求加载过,我们要区分文件已经加载完成或是正在加载中,如果已经完成,我们异步执行该模块的加载完成回调函数(相见F.module方法中对loadModule方法调用部分)
- 如果文件未加载完成,我们要将加载完成回调函数缓存入模块加载完成回调函数容器中(该模块的onload数组容器)。
- 如果依赖模块对应的文件未被要求加载过,那么我们要加载该文件,并将该依赖模块的初始化信息写入模块缓存器中
代码如下:
// 模块缓存器。存储已创建模块 let moduleCache = {}; /** * 异步加载依赖模块所在文件 * @param moduleName 模块路径(id) * @param callback 模块加载完成回调函数 */ function loadModule(moduleName, callback) { let _module; // 如果依赖模块被要求加载过 if(moduleCache[moduleName]) { _module = moduleCache[moduleName]; // 如果模块加载完成 if(_module.status === ‘loaded‘) { // 执行模块加载完成后回调函数 setTimeout(callback(_module.exports), 0); } else { // 缓存该模块所处文件加载完成回调函数 _module.onload.push(callback); } // 模块第一次被依赖引用 } else { // 缓存该模块初始化信息 moduleCache[moduleName] = { // 模块ID moduleName: moduleName, // 模块对应文件加载状态(默认加载中) status: ‘loading‘, // 模块接口 exports: null, // 模块对应文件加载完成回调函数缓冲器 onload: [callback] }; // 加载模块对应文件 loadScript(getUrl(moduleName), moduleName); } }
loadModule方法中在加载模块对应文件时需要引用loadScript加载脚本方法和getUrl获取文件路径方法。这两个方法等实现如下:
function getUrl(moduleName) { // 拼接完整的文件路径字符串,如‘lib/ajax‘ => ‘lib/ajax.js‘ return String(moduleName).replace(/\.js$/g, ‘‘) + ‘.js‘; } function loadScript(src, id) { let _script = document.createElement(‘script‘); // 文件类型 _script.type = ‘text/JavaScript‘; // 确认编码 _script.charset = ‘UTF-8‘; // 异步加载 _script.async = true; // 文件路径 _script.src = src; // 文件id _script.id = id; document.getElementsByTagName(‘head‘)[0].appendChild(_script); }
由getUrl方法可以看出,模块的id和文件的路径应该是一一对应的关系,不然在解析路径时就会出错。不过,也可以将getUrl改造成你喜欢的格式,在这里就不多说了。
设置模块
表面上看设置模块就是执行回调函数的,但实质上它做了三件事:
- 对创建的模块来说,当我的所有依赖模块加载完成时,我要使用该方法;
- 对应被依赖的模块来说,其所在的文件加载后要执行该依赖模块(即创建该模块过程)又间接地使用该方法;
- 对于一个匿名函数来说(F.module方法中无URL参数数据),执行过程中也会使用该方法;
设置模块代码如下:
/** * 设置模块并执行模块构造函数 * @param moduleName 模块ID名称 * @param params 依赖模块 * @param callback 模块构造函数 */ function setModule(moduleName, params, callback) { let fn; // 如果模块被调用过 if(moduleCache[moduleName]) { let _module = moduleCache[moduleName]; // 设置模块已经加载完成 _module.status = ‘loaded‘; // 矫正模块接口 _module.exports = callback ? callback.apply(_module, params) : null; // 执行模块文件加载完成回调函数 while(fn = _module.onload.shift()) { fn(_module.exports); } } else { // 模块不存在(匿名模块),则直接执行构造函数 callback && callback.apply(null, params); } // 删除加载的script标签 deleteScript(moduleName); }
在设置代码执行后,便将加载到head中的script标签删除,纯属个人代码洁癖,可以没有的,删除标签代码如下:
function deleteScript(id) { const deleteJs = document.getElementById(id); console.log(deleteJs); if(deleteJs) { document.getElementsByTagName(‘head‘)[0].removeChild(deleteJs); } }
最后
异步模块模式不仅减少了多人开发过程中变量、方法名被覆盖的问题,而且增加了模块依赖,使开发者不必担心某些方法尚未加载或未加载完成造成的无法使用问题。异步加载部分功能也可以将更多首屏不必要的功能剥离出去,减少首屏加载成本。
demo可以在此看到:
https://github.com/weiruifeng/async