I read the documentation of bun two days ago. I am having a hard time understanding this:
I googled it and found out many people also mentioned this, but no one explained it. Here's some reference:
From my point, the loading of ES modules are synchronous with the static import. For example:
// exporter.mjs
console.log("exporter");
// importer.mjs
import "exporter.mjs"
console.log("importer");
Using this command node importer.mjs
. Well, if the loading of exporter.js
is asynchronous, the output would be "importer" printing first and "exporter" following. While the result is "exporter" printing first and "importer" following.
enter image description here
This proved that the loading of ES modules are synchronous with the static import, in which case we could import ES modules in CommonJS modules. While the article I mentioned above all say "we can't import ES modules in CommonJS modules, we have to use the dynamic import function" , so did the documentation of nodejs. However, we could import ES modules in CommonJS modules in Bun. This is an example:
// exporter.mjs
export let number = 10;
// importer.cjs
const { number } = require("./exporter.mjs");
console.log(number);
Using this command bun importer.cjs
. We can get the output like this.
enter image description here
I am really confused.
The idea that "CommonJS modules are synchronous and ES Modules are asynchronous" by itself might be a bit of an over-simplification which causes this confusion. Let's add some context.
ES Modules go through the following steps:
Load: Asynchronously loads dependencies.
From the spec:
Prepares the module for linking by recursively loading all its dependencies, and returns a promise.
Link: Connects module exports with imports in memory.
From the spec:
Prepare the module for evaluation by transitively resolving all module dependencies and creating a Module Environment Record.
LoadRequestedModules must have completed successfully prior to invoking this method.
Evaluate (and execute): Run the code.
From the spec:
Returns a promise for the evaluation of this module and its dependencies, resolving on successful evaluation or if it has already been evaluated successfully, and rejecting for an evaluation error or if it has already been evaluated unsuccessfully. If the promise is rejected, hosts are expected to handle the promise rejection and rethrow the evaluation error.
Link must have completed successfully prior to invoking this method.
As you can see from the quotes from the JavaScript/ECMAScript spec, various steps are performed asynchronously however steps depend on each other. In practice, this means dependencies will be evaluated and executed before the dependent, which gives the impression that ES modules are synchronous.
CommonJS is different because it uses a function (require
) to import modules. This means, dependencies are only known at runtime (when the code is executed). In other words, code is executed first then dependencies are loaded; as opposed to ES modules which loads dependencies first then executes the code. The synchronous nature of require
comes in because when a require
function is encountered, code after the function is not run until after the require
function has loaded and executed the code within the dependency.
This difference in the order of loading vs execution becomes apparent in a case like this (assuming an entry point of a.js
):
CommonJS | ES Modules | |
---|---|---|
a.js | const { b } = require("./b"); |
import { b } from "./b"; |
b.js | const { a } = require("./a"); |
import { a } from "./a"; |
Logs | b |
b |
In the CommonJS case, a.js
requires b.js
. As require
is synchronous, it blocks the export of a
. As a result, a
is undefined in b.js
.
In the ES Module case, while a.js
also requires b.js
, the link between the a
export in a.js
and import in b.js
is set up before code is executed. As module evaluation and execution occurs asynchronously, a
is exported before the setTimeout
's callback execution meaning a
will have its value initialised to "a"
. Why this happens is because Promises are scheduled in the microtask queue while timers are scheduled in the macrotask queue; and microtasks are executed before macrotasks. This itself is a whole topic that already has answers elsewhere on the internet. I'll include something in the further reading below.