Reputation: 11600
So I thought about ES modules lately and this is how I think they work:
#moduleMap
). It maps modules absolute urls to their exports:#moduleMap = {
"https://something.com/module.mjs": { exportName: "value" }
}
import { x } from "./module1.mjs"
=> all x
references are replaced with
#moduleMap["abs path module1.mjs"].x
(and imported module is being fetched)
export const y = "some value"
=> #moduleMap["abs path to this module"].y = "some value"
(as @Bergi pointed out it's not that simple with exports because exports are not hoisted so dead zone for consts and hoisting for functions are not reflected with just property assignments)
(the above is what's called binding that produce 'live bindings')
As @Bergi pointed out, modules are evaluated eagerly starting from the entry module and evaluating module's imports before the module code itself is executed (with exception for circular dependencies) which practically means the import that was required last will be executed first.
#moduleMap["some module"]
, the browser checks if the module was evaluated
#moduleMap
)#moduleMap["some module"].someImport
That's basically all is happening AFAIK. Am I correct?
Upvotes: 0
Views: 509
Reputation: 664513
export const y = "some value"
=>#moduleMap["abs path to this module"].y = "some value"
(the above is what's called binding that produce 'live bindings')
Yeah, basically - you correctly understood that they all reference the same thing, and when the module reassigns it the importers will notice that.
However, it's a bit more complicated as const y
stays a const
variable declaration, so it still is subject to the temporal dead zone, and function
declarations are still subject to hoisting. This isn't reflected well when you think of exports as properties of an object.
- when evaluation reaches any code that accesses
#moduleMap["some module"]
, the browser checks if the module was evaluated
- if it wasn't evaluated it is evaluated at this point after which the evaluation returns to this place (now the module (or its exports to be exact) is 'cached' in
#moduleMap
)- if it was evaluated the import is accessible from
#moduleMap["some module"].someImport
No. Module evaluation doesn't happen lazily, when the interpreter comes across the first reference to an imported value. Instead, modules are evaluated strictly in the order of the import
statements (starting from the entry module). A module does not start to be evaluated before all of its dependencies have been evaluated (apart from when it has a circular dependency on itself).
Upvotes: 1
Reputation: 2161
You have a pretty good understanding but there are a few anomalies that should be corrected.
In ECMA-262, all modules will have this general shape:
Abstract Module {
Environment // Holds lexical environment for variables declared in module
Namespace // Exotic object that reaches into Environment to access exported values
Instantiate()
Evaluate()
}
There are a lot of different places that modules can come from, so there are "subclasses" of this Abstract Module. The one we are talking about here is called a Source Text Module.
Source Text Module : Abstract Module {
ECMAScriptCode // Concrete syntax tree of actual source text
RequestedModules // List of imports parsed from source text
LocalExportEntries // List of exports parsed from source text
Evaluate() // interprets ECMAScriptCode
}
When a variable is declared in a module (const a = 5
) it is stored in the module's Environment
. If you add an export
declaration to that, it will also show up in LocalExportEntries.
When you import
a module, you are actually grabbing the Namespace
object, which has exotic behaviour, meaning that while it appears to be a normal object, things like getting and setting properties might do different things than what you were expecting.
In the case of Module Namespace Objects, getting a property namespace.a
, actually looks up that property as a name in the associated Environment
.
So if I have two modules, A, and B:
// A
export const a = 5;
// B
import { a } from 'A';
console.log(a);
Module B imports A, and then in module B, a
is bound to A.Namespace.a
. So whenever a
is accessed in module b
, it actually looks it up on A.Namespace
, which looks it up in A.Environment
. (This is how live bindings actually work).
Finally onto the subject of your module map. All modules will be Instantiated before they can be Evaluated. Instantiation is the process of resolving the module graph and preparing the module for Evaluation.
The idea of a "module map" is implementation specific, but for the purposes of browsers and node, it looks like this: Module Map <URL, Abstract Module>
.
A good way to show how browsers/node use this module map is dynamic import()
:
async function import(specifier) {
const referrer = GetActiveScriptOrModule().specifier;
const url = new URL(specifier, referrer);
if (moduleMap.has(url)) {
return moduleMap.get(url).Namespace;
}
const module = await FetchModuleSomehow(url);
moduleMap.set(url, module);
return module.Namespace;
}
You can actually see this exact behaviour in Node.js: https://github.com/nodejs/node/blob/e24fc95398e89759b132d07352b55941e7fb8474/lib/internal/modules/esm/loader.js#L98
Upvotes: 2