ESM 的“异步”到底异步在哪里? 为什么import的模块都是同步执行,却说ESM是异步的?-灵析社区

0offer糕手

## 问题 import a from 'module-a'; import b from 'module-b'; console.log(a); console.log(b); 这几行代码是同步执行的,为什么却说 ESM 是异步的。 ## 谁说ESM是异步的? [https://nodejs.org/api/packages.html](https://link.segmentfault.com/?enc=drFSlgR6t7VMTfJuly16mg%3D%3D.Bz9Y8pwzk7X6sOvShE7A9LU%2F8HNV3rf%2FyoVRyHfIVlUSQz%2BUsBKvj1j7M7cM7wiL) 这里说的 ![image.png](https://wmprod.oss-cn-shanghai.aliyuncs.com/images/20250104/227ba8b50ffa8586ccae905b9ccc737e.png) ## 其他问题 看到几篇文章,大都是说 script标签的加载是异步的,并没有说import是异步 的吧 ![image.png](https://wmprod.oss-cn-shanghai.aliyuncs.com/images/20250104/c56cb649d7989600310e509ed4e28ba0.png) ![image.png](https://wmprod.oss-cn-shanghai.aliyuncs.com/images/20250104/9894d9d733a5c669f1de2de9d7e8fd08.png) ## 补充内容 ![image.png](https://wmprod.oss-cn-shanghai.aliyuncs.com/images/20250104/ee4ed5e8809abbc2d7ccb7be828ab765.png) 异步是说这个?

阅读量:290

点赞量:9

问AI
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. 上述一些内容的措辞其实是不严谨的,只是为了方便你理解所以做了大量简化。