ESM 的异步指的是加载过程(Load)是异步的,而不是说一个 ESM 里面的代码执行过程是异步的。 对于开发者来说确实基本没什么感知。除了像下面这样跟加载顺序有关的(实际上你的代码压根不应该依赖于这种加载顺序),大部分场景下都不影响你写代码。 // module-a.js console.log('a'); module.exports = {}; // module-b.js console.log('b'); module.exports = {}; // index.js console.log(1); const a = require('./module-a'); console.log(2); const b = require('./module-b'); console.log(3); // module-a.js console.log('a'); export default {}; // module-b.js console.log('b'); export default {}; // index.js console.log(1); import a './module-a'; console.log(2); import b './module-b'; console.log(3); 上面的两段代码分别是 CommonJS 和 ESM 的,你可以分别自己建好三个文件运行看看输出的 1、2、3、a、b 的顺序,体会一下区别。 但结合[上一个问题](https://segmentfault.com/q/1010000044199859)来看,感觉题主纠结的点现在变成“**为什么 CommonJS 不能 require() 一个 ESM** ”了。 * * * 先忽略 ESM,我们来看 CommonJS。 为啥要有模块?码农最朴素的愿望就是代码隔离+复用嘛,毕竟你肯定既不想所有代码都写在一个文件里、也不想相同功能的代码到处复制粘贴好几遍。那么文件 A 怎么引用文件 B 里的代码呢?一开始 JS 本身没提供这样的能力,于是上古时代各路大神们就只能自己想各种招数来实现这个事情。 上古时代的事情咱们按下不表,如果你感兴趣可以看我之前写的这篇 [《JavaScript 模块化的历史进程》](https://segmentfault.com/a/1190000023017398)。咱们直接快进到 CommonJS。 CommonJS 里所谓的 `require()` 其实就是一个函数而已,只是这个函数是 Node 里内置的、全局的。那么这个函数干了啥,才实现了我们上面所提的“文件 A 引用文件 B 里的代码呢”?其实很简单,就两步: function require(filePath) { const content = fs.readFileSync(filePath); return eval(content); } 这里我们隐去了路径解析、依赖分析、模块缓存、模块实例化、解决循环引用、包装模块代码避免模块里的变量污染全局、解析模块代码的导出值使其变为函数的返回值等等这些“细枝末节”(其实都很重要,但跟我们要讨论的同步异步无关),剩下的最关键的两行代码其实就是上面这两行: 1. 读取文件内容; 2. 把上面读到的文件内容当作 JS 代码去执行一遍。 所以 CommonJS 里所谓的模块导入,其实就是执行一下 `require()` 这个函数,然后拿到它的返回值而已: const modA = require('module-a'); const modB = require('module-b'); // use modA & modB 而这个过程,即所谓的模块**加载** ,是同步的 —— 因为 `require()` 它是一个同步函数嘛。但是到了浏览器里,事情开始有了问题 —— Node 是基于运行在本地磁盘上考虑的,同步读取一个文件内容是可以被接受的;但浏览器里可是要从远程下载文件的,它可没有类似 `fs.readFileSync` 这种同步下载文件的 API(你可能会说 XMLHttpRequest 里不是支持同步发起 AJAX 么?确实,但代价是它请求过程中其他请求都阻塞、整个页面卡死、EventLoop 停止响应)。所以在浏览器里如果要实现 `require()`,就只能是: function require(filePath) { return ajax({ url: filePath }).then((content) => eval(content)); } 但这样模块加载就变成异步的了,要用到这个模块的时候你就得: require('module-a').then((modA) => { require('module-b').then((modB) => { // use modA & modB }); }); 于是大家就想,反正无论如何浏览器里都得变成异步的,干嘛非得继续用 `require()` 这种形式呢?**就算用了它也跟 Node 里写法不兼容(一个是同步拿返回值即可、一个却得异步拿结果),不如干脆另起炉灶吧** 。这才有了 ESM。 那么回到问题上来,为啥 CommonJS 不在 ESM 提出以后继续改进自身,让 `require()` 也能导入一个 ESM 呢? 原因很简单,因为做不到。 为啥做不到?第一点,前面提到了,`require()` 的实质是 Node 提供的一个内置的、全局的函数,它跟你自定义的 function 没什么区别。而 ESM 的 `import` 和 `export` 语法要求必须写在 Top-Level、是不能被函数包裹的,也就是说你不能这么写: function foo() { import modA from 'module-a'; import modB from 'module-b'; } foo(); 当然你也可以说这是先有鸡还是先有蛋的问题,如果 ESM 一开始设计成不是 Top-Level 的,是不是 CommonJS 就能去模拟了。那确实,但人家不是这么设计的不是?而且还有第二点问题,CommonJS 同样还是解决不了,那就是**ESM 支持 Top-Level Await** : // module.js export const data = await fetch({ url: '/some-where' }); // index.js import data from './module.js'; 这 CommonJS 可就更抓瞎了,毕竟 CommonJS 提出的时候,连 Promise 都没有呢,别提什么 await 了。它怎么也想不到以后还有异步导出这种骚操作。 基于以上两点主要原因(当然还有对于命名导出的处理方式不同、循环引用的处理方式不同等等其他一些原因),因此 CommonJS 无法使 `require()` 支持导入一个 ESM,你只能用 `dynamic import` 这种方式来导入。 GitHub 上对此问题曾经有过一些讨论,感兴趣的话可以去看看:[https://github.com/nodejs/modules/issues/454](https://link.segmentfault.com/?enc=jc1P1vhIWjxIL4pbk8pmag%3D%3D.9s%2BCssyCLR5aw9aeGFKogrADubz3gmCMhzENybBLWjIeel0eb4Jff4ZSg2FOfMZH) P.S. 上述一些内容的措辞其实是不严谨的,只是为了方便你理解所以做了大量简化。