当前位置: 首页 > 知识库问答 >
问题:

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

孔正文
2023-09-10

问题

import a from 'module-a';
import b from 'module-b';
console.log(a);
console.log(b);
这几行代码是同步执行的,为什么却说 ESM 是异步的。

谁说ESM是异步的?

https://nodejs.org/api/packages.html
这里说的
image.png

其他问题

看到几篇文章,大都是说 script标签的加载是异步的,并没有说import是异步 的吧

image.png

image.png

补充内容

image.png
异步是说这个?

共有2个答案

裴华荣
2023-09-10

ESM 的异步指的是加载过程(Load)是异步的,而不是说一个 ESM 里面的代码执行过程是异步的。

对于开发者来说确实基本没什么感知。除了像下面这样跟加载顺序有关的(实际上你的代码压根不应该依赖于这种加载顺序),大部分场景下都不影响你写代码。

// module-a.jsconsole.log('a');module.exports = {};// module-b.jsconsole.log('b');module.exports = {};// index.jsconsole.log(1);const a = require('./module-a');console.log(2);const b = require('./module-b');console.log(3);
// module-a.jsconsole.log('a');export default {};// module-b.jsconsole.log('b');export default {};// index.jsconsole.log(1);import a './module-a';console.log(2);import b './module-b';console.log(3);

上面的两段代码分别是 CommonJS 和 ESM 的,你可以分别自己建好三个文件运行看看输出的 1、2、3、a、b 的顺序,体会一下区别。

但结合上一个问题来看,感觉题主纠结的点现在变成“为什么 CommonJS 不能 require() 一个 ESM”了。


先忽略 ESM,我们来看 CommonJS。

为啥要有模块?码农最朴素的愿望就是代码隔离+复用嘛,毕竟你肯定既不想所有代码都写在一个文件里、也不想相同功能的代码到处复制粘贴好几遍。那么文件 A 怎么引用文件 B 里的代码呢?一开始 JS 本身没提供这样的能力,于是上古时代各路大神们就只能自己想各种招数来实现这个事情。

上古时代的事情咱们按下不表,如果你感兴趣可以看我之前写的这篇 《JavaScript 模块化的历史进程》。咱们直接快进到 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 的 importexport 语法要求必须写在 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.jsexport const data = await fetch({ url: '/some-where' });// index.jsimport data from './module.js';

这 CommonJS 可就更抓瞎了,毕竟 CommonJS 提出的时候,连 Promise 都没有呢,别提什么 await 了。它怎么也想不到以后还有异步导出这种骚操作。

基于以上两点主要原因(当然还有对于命名导出的处理方式不同、循环引用的处理方式不同等等其他一些原因),因此 CommonJS 无法使 require() 支持导入一个 ESM,你只能用 dynamic import 这种方式来导入。

GitHub 上对此问题曾经有过一些讨论,感兴趣的话可以去看看:https://github.com/nodejs/modules/issues/454

P.S. 上述一些内容的措辞其实是不严谨的,只是为了方便你理解所以做了大量简化。

公羊雅达
2023-09-10

异步不只在 JS 内部有,在浏览器网页环境与 JS 引擎之间也有。

ESM 的 JS 不会阻塞网页渲染,所以是异步的。

 类似资料:
  • 然而,在我下面的代码中,我希望在这两个示例中都花费相同的15秒(每个任务5秒),如本文所述。然而,第二个示例只需要5秒,同时运行所有3个示例也需要5秒来完成第二个示例。原来的文章花了5秒,但我把它改成了1秒的延迟,让它更明显。 有没有人能解释一下这是怎么回事,为什么它看起来像线程一样运行?

  • 我刚刚发现,在react函数都是异步的,或者在调用它的函数完成后调用。 现在这两样东西很难消化 在博客中,函数是在函数内部调用的,但是触发函数的原因并不是被调用函数所知道的。 他们为什么要让异步,因为JS是单线程语言,而且这个setState不是WebAPI或服务器调用,所以只能在JS的线程上完成。他们这样做是为了使重新呈现不会停止所有事件侦听器和其他东西,还是有其他设计问题。

  • 本文向大家介绍说说你理解的同步和异步的区别是什么?相关面试题,主要包含被问及说说你理解的同步和异步的区别是什么?时的应答技巧和注意事项,需要的朋友参考一下 同步:指的是同一时间只会执行一个任务,只有当前任务执行结束才会继续执行下一个任务。 异步:指的是人主观上同一时间程序同时进行多个任务,实际上是已极小的时间间隔在不同任务间进行切换来实现的。 JS中指的就是多个任务按照你编码的顺序开始任务,但是任

  • 我目前正在阅读Trevor Burnham的Async Javascript。到目前为止这是一本很棒的书。 他谈到这个片段和console.log在Safari和Chrome控制台中是“异步”的。不幸的是我无法复制这个。代码如下: 如果这是异步的,我会预期结果是books的结果。将console.log()放在事件队列中,直到所有代码执行完毕,然后运行它,它将具有bar属性。 虽然它是同步运行的,

  • 问题内容: 我意识到这是一个基本问题,但是我在其他地方找不到答案。 是同步还是异步? 万一它是异步的,是否有可能在传播了动作之后添加一个回调,这是可能的? 问题答案: AFAIK,调度动作是同步的。如果您愿意解决异步调用,则可以在redux中使用thunk- middleware ,其中将dispatch作为回调函数提供,您可以根据需要调用它。

  • 异步模块 if (isIE8) { require.async('compatible-ie8.js', function(exports){ // ... }); } else if (isIE6) { require.async('compatible-ie6.js', function(exports){ // ... });