为什么需要熟知js模块化?

Rnorth
  • 471

js模块化已经是一个老生常谈的问题了,关于js模块化的文章也非常的多,很多文章都清晰的描述了amd,cmd,umd,commonjs之间的区别。但是因为模块化问题被频频讨论所以有几个点不是很清楚,在此请教各位大佬
1、es6+babel/commonjs已经能解决大部分问题了,详细了解amd和cmd和umd是为了解决webpack1这种的落后框架才需要继续去了解其本质区别嘛? 而且事实上现在有许多的框架可以方便的帮我们处理其模块化方式,为什么模块化的问题还是被频频讨论。或者说如果不了解amd和cmd这种的本质区别有什么问题是在开发过程中无法处理的,还或者es6+babel无法适用于哪些场景?

2、尽管大部分资料都能告诉我们amd和cmd一个就近依赖一个依赖置顶,一个requirejs推出的一个是seajs推出的、但是很少有资料能通透的讲出这些模块化规范的本质区别,请问这些规范的本质区别是什么?

3、这些规范当中哪些是同步哪些是异步?

回复
阅读 1.1k
2 个回答
✓ 已被采纳
  • AMD 是 RequireJS 提出来的规范(Web)
  • CMD 是 SeaJS 提出来的规范(Web)
  • CJS(CommonJS) 是 Nodejs 提出来的规范(Node)
  • UMD 是为了兼容 Web 和 Node 环境下的模块规范(Web+Node),使一个模块可以随处可用
  • ESM(ECMAScript Module) 这个规范来得比较晚,但它才是正室,是写在 ECMAScript 规范中的

ESM 目前已经在用的 import/export 是静态加载,可以看作是同步的,因为它要加载完成之后才会执行下面的脚本。不过还有一个 import() 用于动态加载,它返回一个 Promise 对象,是异步的。

构建工具可以根据配置把 ESM 转译成上述各种规范,所以……学正室就好。

还好,最后说明一下,同步(静态)/异步(动态)的本质区别:

require(...)
// 或者 import ...

doSomething();
// 上述过程完成之后才会执行这里的代码
// 这是同步
require(..., cb);
// 或者 import().then(cb);

doSomething();
// 上述过程未完成就会执行这里的代码
// 这是异步

// 异步的情况下,加载完成会执行 cb(callback,回调)

同步改异步只需要加 Promise 封装即可;
异步改同步使用 await 关键字,参考:理解 JavaScript 的 async/await


来看下 RequireJS 的模块定义语法,这是官方示例和解释:

requirejs(["helper/util"], function(util) {
    //This function is called when scripts/helper/util.js is loaded.
    //If util.js calls define(), then this function is not fired until
    //util's dependencies have loaded, and the util argument will hold
    //the module value for "helper/util".
});

就是说,helper/util 加载完成之后才会调用 function(util) {...},而模块加载。这里 function(util) 是当前模块的工厂函数。这个加载过程大概是这样的:

  • 如果 helper/util 在缓存中,直接算加载完成,将缓存中的 helper/util 模块导出项作为当前模块工厂函数的 util 参数传入
  • 如果 helper/util 不在缓存中,等待加载。加载 helper/util 的过程中如果需要加载其他依赖项,那就继续递归等待,所有依赖加载完成,helper/util 加载成功,缓存其导出项并将其导出项作为 util 参数传入当前模块工厂

所以,这个过程是异步的,也就是说 AMD 是异步的。调用 requirejs 的时候,是异步加载依赖项,完成之后调用后面的回调(工厂函数)


接着看 SeaJS,

define(function(require, exports, module) {
  var $ = require('jquery');

  exports.sayHello = function() {
    $('#hello').toggle('slow');
  };
});

define 直接定义了一个工厂函数,其中 require 是作为一个参数传进来的。注意到这句:

var $ = require('jquery')

require 并没有等待加载 jquery 完成的过程(没有回调,也没有 await 这种典型的异步特征),就将结果赋值给 $ 了,这说明 jquery 早就已经加载进来了。那 jquery 是什么时候加载进来的呢?注意到下面主程序中的代码:

seajs.use(['./hello', 'jquery'], function(hello, $) {
  $('#beautiful-sea').click(hello.sayHello);
});

这是一个典型的异步形式。所以 SeaJS 是在 use 的时候一次性分析完所有依赖模块,分析他们的依赖关系,做好缓存之后再来执行的主程序。所以 CMD 的模块工厂是同步的,但是最上层的 use 是异步的(浏览器 Ajax 只能异步加载,不异步也不行啊)


CommonJS,这个不用官方的例子,随便都能写两个

const path = require("path");

const current = path.resolve("./");

这个也很明显,require() 是典型的同步调用(没有回调,也没有 await 等),所以 CJS 模块也是同步的。因为 Node 有自己的 Runtime,它完全可以做到 SeaJS 做不到的事情,把加载过程做成完全的同步过程。


ESM 的 import 其实也是同步的,浏览器在自己控制范围内是也可以同步加载的,比如原始的 <script> 就是阻塞的,所以 import 可以同样实现。Node 就不用说了,一个道理。只能说自己能控制运行时真好,可怜的 SeaJS。示例不用写了,跟 CommonJS 的那个道理一样,就当是不同的语法干同样的事情吧。

另外还有一个 import(),是这样用的

const util = await import("./util");

或者

import("./util").then(util => { ... });

这是典型的异步加载。所以 EJS 默认支持同步加载模块,通过 import() 提供异步(动态)加载能力。

  1. 如果你现在才开始学习,那么肯定先学 ESM 和 CommonJS。前者是标准,后者更普及。
  2. 了解各种规范,就跟我们学历史一样,以古鉴今,避免重蹈覆辙。
  3. “为什么模块化的问题还是被频频讨论”——因为有面试需求,还有公众号传播需求
  4. 规范的本质去看各自的官网
  5. 目前没有异步的模块管理规范,我认为是因为异步会大幅增加复杂性。webpack 提供异步支持,但其实是在它自己的架构下。
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
你知道吗?

宣传栏