异步模块模式

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-12-15

异步模块模式(AMD: Asynchronous Module Definition):请求发出后,继续其他业务逻辑,直到模块加载完成后执行后续的逻辑,实现模块开发中对模块加载完成后的引用。

浏览器不同于服务器环境,在浏览器中对文件的加载是异步的,因此要使用未加载文件中的某些模块方法时必然经历文件加载过程。因此对未加载文件中模块引用,同步模块方式是无能为力的。

// main.js
(function(F) {
  var moduleCache = {};

  function loadModule(moduleName, callback) {
    var _module;
    if (moduleCache[moduleName]) { // 缓存过
      _module = moduleCache[moduleName];

      // 如果模块加载完成
      if (_module.status === 'loaded') {
        // 执行模块加载完成会带哦函数
        setTimeout(callback(_module.exports), 0)
      } else {
        // 缓存该模块所处文件加载完成回调函数
        _module.onload.push(callback);
      }
    } else { // 初次引用
      // 初始化模块信息
      moduleCache[moduleName] = {
        moduleName, // 模块名称
        status: 'loading', // 加载状态
        exports: null, // 模块接口
        onload: [callback], // 加载完成回调缓存数组
      };
      // 加载对应模块文件
      loadScript(getUrl(moduleName));
    }
  }

  // 获取文件路径
  function getUrl(moduleName) {
    // 保证完整的js路径名 如传入 lib/ajax => lib/ajax.js 如果传入lib/ajax.js则不变
    return String(moduleName).replace(/\.js$/g, '') + '.js';
  }

  // 加载脚本文件
  function loadScript(src) {
    var _script = document.createElement('script');
    _script.type = 'text/JavaScript';
    _script.async = true; // 异步加载
    _script.src = src;
    document.getElementsByTagName('head')[0].appendChild(_script); // 插入界面
  }

  // 不依赖其他模块,或者依赖的模块全部加载了,调用setModule “构造”该模块
  function setModule(moduleName, params, callback) {
    var _module, fn;
    if (moduleCache[moduleName]) { // 被调用过
      _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);
    }
  }

  // 注册模块或调用模块
  // 1、注册模块 
  // 举例1:F.module('lib/dom', function() {}) 
  // 举例2:F.module('lib/event', ['lib/dom'], function(dom) {}  代表event模块依赖dom模块
  // 参数列表:
  //    模块名称(最好与加载路径一致lib/dom.js)
  //    依赖的模块列表(为数组,若没有可不传)
  //    该模块的构造函数

  // 2、调用模块 举例3:F.module(['lib/event', 'lib/dom'], function(events, dom) {})
  // 参数列表:依赖的模块列表(为数组)、回调
  F.module = function() {
    var args = Array.from(arguments);
    var callback = args.pop(); // 取最后一个参数作为模块构造函数或者回调

    // 使用pop之后,再取args[args.length-1] 就是倒数第二个了
    // 如果是数组,就取其作为依赖模块列表,否则声明一个空数组代替依赖模块列表
    var deps = (args.length && args[args.length -1] instanceof Array) ? args.pop() : [];

    // 如果参数列表在两次pop后还有,就认为该参数是注册模块时的模块url
    // 也为对应模块的文件路径
    // 如果只有两个参数,说明是调用模块,就没有需要加载的url了
    var url = args.length ? args.pop() : null;

    // 依赖模块序列
    var params = [];

    // 未加载的依赖模块数量统计
    var depsCount = 0;

    var len = deps.length;

    // 遍历依赖模块列表 比如举例2中event依赖dom模块
    // 有需要依赖的模块,就先去加载依赖模块,不管是调用还是注册
    if (len) {
      for (let i = 0; i < len; i++) {
        depsCount++;
        loadModule(deps[i], function(mod) {
          params[i] = mod;
          // 依赖模块加载完成,依赖模块数量--
          depsCount--;
          // 依赖模块全部加载
          if (depsCount === 0) {
            setModule(url, params, callback)
          }
        })
      }
    } else {
      // 没有依赖模块,则只能是注册模块,此时url就是模块名称,而不是需要异步加载的url地址 
      // 比如举例1中的dom模块,不依赖其他模块
      setModule(url, [], callback)
    }
  }
})((function() {
  // 创建模块管理器对象,并保存在全局作用域中,然后作为参数传递给闭包
  return window.F = {};
})());

声明两个模块event和dom

// lib/event.js  依赖lib/dom.js的加载
F.module('lib/event', ['lib/dom'], function(dom) {
  var events = {
    on: function(id, type, fn) {
      dom.g(id)['on'+ type] = fn;
    }
  }
  return events;
})
// lib/dom.js
F.module('lib/dom', function() {
  return {
    g: function(id) {
      return document.getElementById(id);
    },
    html: function(id, html) {
      if (html) {
        this.g(id).innerHTML = html;
      } else {
        return this.g(id).innerHTML;
      }
    }
  }
})

在业务处使用:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="test">test</div>
  <script src="main.js"></script>
  <script>
    // 业务逻辑
    // 依赖lib/event.js 和 lib/dom.js的加载
    F.module(['lib/event', 'lib/dom'], function(events, dom) {
      events.on('test', 'click', function() {
        console.log('test success')
      })
    })
  </script>
</body>
</html>

上面例子的代码逻辑和业务逻辑如下图。

模块化开发不仅解决了系统的复杂性问题,而且减少了多人开发中变量、方法名被覆盖的问题,通过其强大的命名空间管理,使模块的结构更合理,通过对模块的引用,提高了模块代码复用率。异步模块模式在此基础上增加了模块依赖,使开发者不必担心某些方法尚未加载或未加载完全造成的无法使用问题。异步加载部分功能也可将首屏不必要的功能剥离出去,减少首屏加载成本。