ES6导入的定义执行顺序是什么?

62

我尝试在互联网上搜索导入模块的执行顺序。例如,假设我有以下代码:

import "one"
import "two"
console.log("three");

其中one.jstwo.js定义如下:

// one.js
console.log("one");

// two.js
console.log("two");

控制台输出是否保证为:

one
two
three

还是未定义的吗?


导入是同步的,因此输出顺序是有保证的。控制台显示内容在技术上是异步的,但这并不重要,因为它是缓冲的。 - dandavis
1
无论答案如何,经验法则是:每当您需要特定的评估顺序时,请明确声明您的依赖关系,并使用import导入 - Bergi
2个回答

50
JavaScript模块是异步评估的。然而,在导入模块的代码体执行之前,所有导入都会被评估。这使得JavaScript模块与Node中的CommonJS模块或没有async属性的<script>标签不同。当涉及到它们如何加载时,JavaScript模块更接近AMD规范。有关更多详细信息,请参见Axel Rauschmayer的Exploring ES6第16.6.1节
因此,在提问者提供的示例中,无法保证执行顺序。有两种可能的结果。我们可以在控制台看到如下内容:
one
two
three

或者我们可能会看到这个:

two
one
three

换句话说,这两个导入的模块可以按任意顺序执行它们的console.log()调用; 它们相互之间是异步的。但它们肯定会在导入它们的模块体之前被执行,因此"three"保证是最后一个被记录的。
当使用顶级await语句(现在已在Chrome中实现)时,可以观察到模块的异步性。例如,假设我们稍微修改提问者的示例:
// main.js
import './one.js';
import './two.js';
console.log('three');

// one.js
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('one');

// two.js
console.log('two');

当我们运行main.js时,在控制台中我们会看到以下内容(为了说明添加了时间戳):
[0s] two
[1s] one
[1s] three

ES2020更新

根据petamoriken的回答,从ES2020开始,非异步模块的评估顺序是有保障的。因此,如果你知道你导入的模块中没有顶层await语句,它们将按照导入顺序执行。对于提问者的示例,控制台输出将始终为:

one
two
three

4
“导入的ES6模块是异步执行的”有相关参考资料吗? - Benjamin Gruenbaum
3
据我所知,ES2015模块并不会异步地被导入,而是它们的加载方式完全取决于模块加载器。 - Benjamin Gruenbaum
1
@BenjaminGruenbaum 我只能引用我自己的回答:“更多细节请参见Axel Rauschmayer的《探索ES6》第16.6.1节。” - McMath
2
@BenjaminGruenbaum 是的,这就是为什么我加上了“没有现代浏览器实现ES6模块”的警告。我不知道像Babel这样的转译器在这方面是否遵循原始规范。我怀疑大多数这样的转译器都会像你建议的那样同步导入。但我的答案是关于原始规范的。 - McMath
3
我实际上是那本书的评论者。我的观点是,决定如何加载模块并不取决于ES模块规范,而取决于已经被加载的模块规范。如果我没记错的话,规范只要求在代码执行时加载所有模块。 - Benjamin Gruenbaum
显示剩余7条评论

8
根据最新规范InnerModuleEvaluationmodule.ExecuteModule()的顺序是有保证的,因为[[RequestedModules]]是源代码出现的有序列表
// 16.2.1.5.2.1 rough sketch
function InnerModuleEvaluation(module, stack, index) {

  // ...

  // 8
  module.[[PendingAsyncDependencies]] = 0;

  // ...

  // 11: resolve dependencies (source code occurrences order)
  for (required of module.[[RequestedModules]]) {
    let requiredModule = HostResolveImportedModule(module, required);
    // **recursive**
    InnerModuleEvaluation(requiredModule, stack, index);

    // ...

    if (requiredModule.[[AsyncEvaluation]]) {
      ++module.[[PendingAsyncDependencies]];
    }
  }

  // 12: execute
  if (module.[[PendingAsyncDependencies]] > 0 || module.[[HasTLA]]) {
    module.[[AsyncEvaluation]] = true;
    if (module.[[PendingAsyncDependencies]] === 0) {
      ExecuteAsyncModule(module);
    }
  } else {
    module.ExecuteModule();
  }

  // ...

}

控制台输出始终如下:
one
two
three

对于非异步模块,这在 ES020 中是正确的。 - McMath
这是正确的,尽管有一个微妙的点,那就是如果有多个条目进入模块图,两个和三个可能已经被导入。在这种情况下,仅打印三是可能的,尽管在这种情况下,一、二仍然首先被打印,但它们可能已经任意地过去打印了很远。这里的一个含义是,如果有不同的入口点,则two,one,three是可能的顺序(尽管three始终是最后一个)。 - Jamesernator

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接