# 异步模块模式

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

异步模块模式会涉及到模块依赖,根据依赖模块加载文件,加载文件成功后执行引用模块时生命的回调函数等一些技术的实现。

# 闭包环境

首先要创建一个闭包,目的是封闭已创建的模块,防止外界对其直接访问,并在闭包中创建模块管理器 F ,并作为接口保存在全局作用域中。

// asyncModuleManager.js

;(function(){

  const moduleCache = {}
  // ...
  F.module = function (url,modDeps,modCallback){
    // ...
  }

})((function(){
  
  return window.F = {}

})())

# 创建和调度模块

在异步模块中,创建和调度模块时,需要遍历所有的依赖模块,并且这些依赖都加载完成才可执行回调函数。依赖模块如果依赖其他的模块,则同理以此类推。

这里我们创建和调度模块都使用F.module方法,使用这些模块时,可以认为当前是一个匿名模块。

// asyncModuleManager.js

(function () {
  const moduleCache = {};
  const setModule = function(){}
  const loadModule = function(){};
  // ...
  F.module = function (url, modDeps, modCallback) {
    const args = [].slice.call(arguments);
    let callback = args.pop()
    let deps = (args.length && args[args.length - 1] instanceof Array) ? args.pop() : [];
    let 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) {
            params[i] = mod;
            depsCount--;
            // 等待最后一个依赖加载完成
            if (depsCount === 0) {
              setModule(url, params, callback)
            }
          })
        })(i)
        i++
      }
    } else {
      // 无依赖模块,匿名模块
      setModule(url, [], callback)
    }
  };
})(
  (function () {
    return (window.F = {});
  })()
);

# 加载模块

现在我们要来实现异步模块的加载函数

const moduleCache = {};
// ...
const loadModule = function (moduleName, callback) {
  if (moduleCache[moduleName]) {
    // 已被加载过
    const _module = moduleCache[moduleName];
    if (_module.status === "loaded") {
      // 如果缓存模块加载完成
      setTimeout(callback(_module.exports), 0);
    } else {
      // 缓存回调函数
      _module.onload.push(callback)
    }
  } else {
    // 第一次加载
    moduleCache[moduleName] = {
      moduleName: moduleName,
      status: "loading",
      exports: null,
      onload: [callback]
    };
    loadScript(getUrl(moduleName))
  }
}
const getUrl = function(moduleName){};
const loadScript = function(src){}

还有两个配套方法的实现

const getUrl = function(moduleName){
  // lib/ajax => lib/ajax.js
  return String(moduleName).replace(/\.js$/g,"") + ".js";
};
const loadScript = function(src){
  const _script = document.createElement("script");
  _script.type = "text/javascript";
  _script.async = true;
  _script.src = src;
  document.getElementsByTagName("head")[0].appendChild(_script)
}

# 设置模块

最后来实现核心方法 setModule

表面上看,这个方法就是执行模块回调函数用的,但实质上这个方法做了三件事:

  • 对创建的模块来说,当其所依赖的模块加载完成时,使用该方法。
  • 对于被依赖的模块来说,当模块加载完成时,间接的使用该方法。
  • 对于一个匿名模块来说,执行过程中也会使用该方法
const setModule = function(moduleName, params,callback){
  if(moduleCache[moduleName]){
    // 如果模块被调用过
    const _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)
  }    
}

# 学以致用

  • 定义无依赖模块 F.module(String, Function)
  • 定义有依赖模块 F.module(String, Array, Function)
  • 使用一个或多个模块 F.module(Array, Function)

定义模块 lib/dom

// 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
      }
    }
  }
})

定义模块 lib/event 此模块依赖 lib/dom

// lib/event.js
F.module("lib/event", ["lib/dom"], function (dom) {
  const events = {
    on: function (id, type, fn) {
      dom.g(id)["on" + type] = fn
    }
  }
  return events;
})

使用模块 lib/eventlib/dom 注意模块和参数顺序对应

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="./asyncModuleManager.js"></script>
  <title>asyncModule</title>
</head>

<body>
  <div id="demo">Hello</div>
  <script>
    F.module(["lib/event", "lib/dom"], function (events, dom) {
      events.on("demo", "click", function () {
        dom.html("demo", "success");
      })
    })
  </script>
</body>

</html>

页面加载完成时,<head> 标签内容如下

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="./asyncModuleManager.js"></script>
  <title>asyncModule</title>
  <script type="text/javascript" async="" src="lib/event.js"></script>
  <script type="text/javascript" async="" src="lib/dom.js"></script>
</head>

# 总结

模块化开发不仅解决了系统的复杂性问题,而且减少了多人开发中变量污染的问题。通过其强大的命名空间管理,使模块的结构更加合理。通过对模块的引用提高了代码的复用率。

异步模块模式在此基础上增加了模块依赖,使开发者不必担心某些方法未加载或未加载完全造成的无法使用问题。

异步加载部分功能也可将更多首屏不必要的功能剥离出去,减少首屏加载成本。