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