众所周知,模块化开是会将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控、可维护、可拓展,提升模块复用率。而在js中,异步模块模式的状况则比较多,所谓异步模块模式,是在请求发出后,继续其余业务逻辑,直到模块加载完成后执行后续的逻辑,实现模块开发中对模块加载完成后的引用。git
今天就来分析一下异步加载模块,本文经过建立与调度模块、加载模块和设置模块三个方面来分析github
建立与调度方法集模块建立方法于一身。在这个方法中腰遍历全部依赖模块,并判断全部模块都存在才可执行回调函数,不然加载相应文件,直到文件加载完成才执行回调函数。ajax
实现代码以下:数组
/** * 建立或调用模块方法 * @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方法目的是加载依赖模块对应的文件并执行回调函数。对此可分三种状况处理:app
代码以下:异步
// 模块缓存器。存储已建立模块 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获取文件路径方法。这两个方法等实现以下:async
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改形成你喜欢的格式,在这里就很少说了。模块化
表面上看设置模块就是执行回调函数的,但实质上它作了三件事:函数
设置模块代码以下:
/** * 设置模块并执行模块构造函数 * @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能够在此看到: