What is the defined execution order of ES6 imports?
Asked Answered
B

2

64

I've tried searching the internet for the execution order of imported modules. For instance, let's say I have the following code:

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

Where one.js and two.js are defined as follows:

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

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

Is the console output guaranteed to be:

one
two
three

Or is it undefined?

Bristow answered 22/2, 2016 at 10:33 Comment(2)
import is sync, so the output order is guaranteed. the console showing stuff is technically async, but that doesn't matter because it's buffered.Alienable
Regardless of the answer, the rule of thumb is: Whenever you require a certain evaluation order, explicitly declare your dependencies with an import.Kettledrummer
D
52

JavaScript modules are evaluated asynchronously. However, all imports are evaluated prior to the body of module doing the importing. This makes JavaScript modules different from CommonJS modules in Node or <script> tags without the async attribute. JavaScript modules are closer to the AMD spec when it comes to how they are loaded. For more detail, see section 16.6.1 of Exploring ES6 by Axel Rauschmayer.

Thus, in the example provided by the questioner, the order of execution cannot be guaranteed. There are two possible outcomes. We might see this in the console:

one
two
three

Or we might see this:

two
one
three

In other words, the two imported modules could execute their console.log() calls in any order; they are asynchronous with respect to one another. But they will definitely be executed prior to the body of the module that imports them, so "three" is guaranteed to be logged last.

The asynchronicity of modules can be observed when using top-level await statements (now implemented in Chrome). For example, suppose we modify the questioner's example slightly:

// 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');

When we run main.js, we see the following in the console (with timestamps added for illustration):

[0s] two
[1s] one
[1s] three

Update as of ES2020

Per petamoriken's answer, it looks like evaluation order is guaranteed for non-async modules as of ES2020. So, if you know none of the modules you're importing contain top-level await statements, they will be executed in the order in which they are imported. In the case of the questioner's example, the console output will always be:

one
two
three
Deliquescence answered 22/2, 2016 at 11:17 Comment(12)
Any reference on "Imported ES6 modules are executed asynchronously."?Aussie
As far as I know, ES2015 modules are not imported asynchronously, rather - how they load is up to the module loader entirely.Aussie
@BenjaminGruenbaum I can only quote my own answer: "For more detail, see section 16.6.1 of Exploring ES6 by Axel Rauschmayer."Deliquescence
@BenjaminGruenbaum Yes, that's why I add the caveat that "no modern browser implements ES6 modules. I don't know if transpilers such as Babel follow the original specification in this respect." I suspect that most such transpilers do import synchronously, as you suggest. But my answer is about the original specification.Deliquescence
I'm actually a reviewer of that book. The point I'm making is that it's not up to the ES module spec to decide it - it's up to the module loaded spec to decide how to load the modules. IIRC the spec only requires that all the modules are loaded when code executes.Aussie
@BenjaminGruenbaum OK, it sounds like you know more about this than I do. Feel free to provide your own answer. I'm happy to take this one down once I'm shown to be mistaken.Deliquescence
I don't want you to take it down, I'm not even 100% sure about it myself. Your answer is almost right except the part that dictates how it works in browsers should indicate it happens this way because of the browser module loader spec and not the ES spec. Node for example is not bound by it.Aussie
@BenjaminGruenbaum I'm pretty sure the intention is to allow asynchronous and concurrent resolution/loading/initialisation of modules (also see this wording). I have to admit though that the spec is written in a pretty synchronous style (especially the module evaluation) which is all part of a single TopLevelModuleEvaluationJob. It's even pretty sequential (with a well-defined order), given that the [[RequestedModules]] list is iterated. So…Kettledrummer
We would have to assume that HostResolveImportedModule is asynchronous (like: wait an implementation-defined time, similar to how XHR is specced) during which other job queues can run, and that it either recursively schedules module evaluation jobs or just calls the module evaluation method of the loaded modules as soon as there are no more unresolved dependencies.Kettledrummer
@Kettledrummer Thanks for your contribution. The spec still seems ambiguous to me on this question. I'm not sure I quite understand why HostResolveImportedModule must be asynchronous; I get the impression that it would be implementation-dependent. It may just be that I am in over my head on this. In any case, I invite you to make or suggest any edits, and I will gladly yield to a better answer if you have one.Deliquescence
If modules wouldn't be executed in import order (within a single module, not across multiple html script tags) then polyfill libraries like core-js would not work reliably. I think this answer is a bit misleading, even though I don't have a good reference in the spec to prove it.Austerity
@Austerity All imports are executed before the rest of the code in a file, so a polyfill library (or any other library) will have fully executed by time you use it. The only scenario that would be a problem would be if you were to import core-js, followed by some-other-module, which depends on core-js. But in that case, some-other-module should itself import core-js. That said, this is an old answer. Now that browsers have in fact implemented modules, it's possible that imports are executed consecutively in practice. I should probably check and update the answer.Deliquescence
D
10

According to the latest specification InnerModuleEvaluation, the order of module.ExecuteModule() is guaranteed since [[RequestedModules]] is an ordered list of source code occurrences.

// 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();
  }

  // ...

}

The console output is always as follows:

one
two
three
Deering answered 3/11, 2021 at 8:56 Comment(2)
This is correct as of ES020 for non-async modules.Deliquescence
This is correct, although there is a subtle point, and that's that if there are multiple entries into the module graph, two and three may already have been imported. In which case just printing three is possible, although in that case one,two have still been printed first, however they may have printed arbitrarily far in the past. One implication here is that two,one,three is a possible order if there is a different entry point (although three is still always last).Forlorn

© 2022 - 2024 — McMap. All rights reserved.