Ruan Yifeng mentioned in his introduction to ES6 that ES6 modules have some significant differences from CommonJS modules:
- The CommonJS module prints a copy of the value, the ES6 module prints a reference to the value.
- The CommonJS module is run time loaded, and the ES6 module is compile time output interface.
A closer reading of the differences mentioned by Ruan will lead to many questions:
- Why does the CommonJS module output a copy of a value? What do the details look like?
- What do you mean
Run time loading
? - What do you mean
Compile-time output interface
? - Why does the ES6 module output references to values?
Hence this article, which tries to discuss the ESM module and CommonJS module clearly.
The historical background of CommonJS
CommonJS was founded in January 2009 by Mozilla engineer Kevin Dangoor and was originally named ServerJS. In August 2009, the project was renamed CommonJS. Designed to address the lack of modular standards in Javascript.
Node.js later adopted the CommonJS module specification as well.
Since CommonJS is not part of the ECMAScript standard, it is important to realize that things like Module and require are not JS keywords, just objects or functions.
We can view details in print Module, require:
console.log(module);
console.log(require);
// out:
Module {
id: '. '.path: '/Users/xxx/Desktop/esm_commonjs/commonJS'.exports: {},
filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js'.loaded: false.children: [].paths: [
'/Users/xxx/Desktop/esm_commonjs/commonJS/node_modules'.'/Users/xxx/Desktop/esm_commonjs/node_modules'.'/Users/xxx/Desktop/node_modules'.'/Users/xxx/node_modules'.'/Users/node_modules'.'/node_modules']} [Function: require] {
resolve: [Function: resolve] { paths: [Function: paths] },
main: Module {
id: '. '.path: '/Users/xxx/Desktop/esm_commonjs/commonJS'.exports: {},
filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js'.loaded: false.children: [].paths: [
'/Users/xxx/Desktop/esm_commonjs/commonJS/node_modules'.'/Users/xxx/Desktop/esm_commonjs/node_modules'.'/Users/xxx/Desktop/node_modules'.'/Users/xxx/node_modules'.'/Users/node_modules'.'/node_modules']},extensions: [Object: null prototype] {
'.js': [Function (anonymous)],
'.json': [Function (anonymous)],
'.node': [Function (anonymous)]
},
cache: [Object: null prototype] {
'/Users/xxx/Desktop/esm_commonjs/commonJS/c.js': Module {
id: '. '.path: '/Users/xxx/Desktop/esm_commonjs/commonJS'.exports: {},
filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js'.loaded: false.children: [].paths: [Array]}}}Copy the code
You can see that module is an object, require is a function, and that’s it.
Let’s highlight some of the attributes in the Module:
exports
: this is themodule.exports
The corresponding value is currently an empty object, since no value has been assigned to it.loaded
: indicates whether the current module is loaded successfully.paths
: load path of node module, this section will not expand, interested can seeThe node document
The require function also has some notable properties:
main
Refers to the current current reference to its own module, so similar to Python__name__ == '__main__'
, node can also be usedrequire.main === module
To determine whether the program is started with the current module.extensions
Represents several modes of loading modules currently supported by Node.cache
Represents the cache of module loads in Node, i.e., when a module is loaded once, afterrequire
Instead of loading it again, it will be read from the cache.
As mentioned earlier, in CommonJS module is an object and require is a function. The corresponding import and export in the ESM are keywords that are part of the ECMAScript standard. It’s critical to understand the difference.
Let’s look at a couple of CommonJS examples
Take a look at the following CommonJS examples to see if you can accurately predict the result:
Example 1: Assign a value to a simple type outside the module:
// a.js
let val = 1;
const setVal = (newVal) = > {
val = newVal
}
module.exports = {
val,
setVal
}
// b.js
const { val, setVal } = require('./a.js')
console.log(val);
setVal(101);
console.log(val);
Copy the code
Run B. js, and the output result is:
1
1
Copy the code
Example 2: Assigning a reference type outside the module:
// a.js
let obj = {
val: 1
};
const setVal = (newVal) = > {
obj.val = newVal
}
module.exports = {
obj,
setVal
}
// b.js
const { obj, setVal } = require('./a.js')
console.log(obj);
setVal(101);
console.log(obj);
Copy the code
Run B. js, and the output result is:
{ val: 1 }
{ val: 101 }
Copy the code
Example 3: Change the simple type after exporting in the module:
// a.js
let val = 1;
setTimeout(() = > {
val = 101;
}, 100)
module.exports = {
val
}
// b.js
const { val } = require('./a.js')
console.log(val);
setTimeout(() = > {
console.log(val);
}, 200)
Copy the code
Run B. js, and the output result is:
1
1
Copy the code
Example 4: export from module exports again with module.exports:
// a.js
setTimeout(() = > {
module.exports = {
val: 101}},100)
module.exports = {
val: 1
}
// b.js
const a = require('./a.js')
console.log(a);
setTimeout(() = > {
console.log(a);
}, 200)
Copy the code
Run B. js, and the output result is:
{ val: 1 }
{ val: 1 }
Copy the code
Example 5, export again with exports after exporting within the module:
// a.js
setTimeout(() = > {
module.exports.val = 101;
}, 100)
module.exports.val = 1
// b.js
const a = require('./a.js')
console.log(a);
setTimeout(() = > {
console.log(a);
}, 200)
Copy the code
Run B. js, and the output result is:
{ val: 1 }
{ val: 101 }
Copy the code
What explains the above example? No magic! Break down the details of CommonJS value copy
Take out JS the most simple thinking, to analyze the above examples of all kinds of phenomena.
In example 1, the code can be simplified to:
const myModule = {
exports: {}}let val = 1;
const setVal = (newVal) = > {
val = newVal
}
myModule.exports = {
val,
setVal
}
const { val: useVal, setVal: useSetVal } = myModule.exports
console.log(useVal);
useSetVal(101)
console.log(useVal);
Copy the code
In example 2, the code can be simplified as:
const myModule = {
exports: {}}let obj = {
val: 1
};
const setVal = (newVal) = > {
obj.val = newVal
}
myModule.exports = {
obj,
setVal
}
const { obj: useObj, setVal: useSetVal } = myModule.exports
console.log(useObj);
useSetVal(101)
console.log(useObj);
Copy the code
In example 3, the code can be simplified to:
const myModule = {
exports: {}}let val = 1;
setTimeout(() = > {
val = 101;
}, 100)
myModule.exports = {
val
}
const { val: useVal } = myModule.exports
console.log(useVal);
setTimeout(() = > {
console.log(useVal);
}, 200)
Copy the code
In example 4, the code can be simplified as:
const myModule = {
exports: {}}setTimeout(() = > {
myModule.exports = {
val: 101}},100)
myModule.exports = {
val: 1
}
const useA = myModule.exports
console.log(useA);
setTimeout(() = > {
console.log(useA);
}, 200)
Copy the code
In example 5, the code can be simplified as:
const myModule = {
exports: {}}setTimeout(() = > {
myModule.exports.val = 101;
}, 100)
myModule.exports.val = 1;
const useA = myModule.exports
console.log(useA);
setTimeout(() = > {
console.log(useA);
}, 200)
Copy the code
Try running the code above and see that it matches the CommonJS output. So CommonJS isn’t magic, it’s just the simplest JS code you write every day.
The value copy occurs at the moment the module.exports value is assigned, for example:
let val = 1;
module.exports = {
val
}
Copy the code
All it did was give module.exports a new object in which there was a key called val whose value was the same as that of the current module.
CommonJS concrete implementation
In order to understand CommonJS more thoroughly, let’s write a simple module loader, mainly reference nodeJS source code;
In the node v16. X module mainly implemented in the lib/internal/modules/CJS/loader. The js file.
In Node v4. X, module is implemented in lib/module.js.
The following implementation mainly refers to the implementation in Node V4.x, since older versions are relatively “clean” and easier to catch details.
In addition, in-depth node.js module loading mechanism, written require function this article is also very good, many of the following implementation is also referred to this article.
To distinguish it from the official Module name, our own class is named MyModule:
function MyModule(id = ' ') {
this.id = id; // Module path
this.exports = {}; // The exported object is placed here and initialized as an empty object
this.loaded = false; // Indicates whether the current module is loaded
}
Copy the code
Method the require
The require method is an instance of the Module class, the content is very simple, first do some parameters check, and then call the module. _load method, the source code is here, this example for the sake of simplicity, remove some of the determination:
MyModule.prototype.require = function (id) {
return MyModule._load(id);
}
Copy the code
Require is a very simple function that wraps around the _load function, which does the following:
- First check whether the requested module already exists in the cache, if so, return the cache module directly
exports
- If it is not in the cache, create one
Module
Instance, put the instance in the cache, use this instance to load the corresponding module, and return the module’sexports
MyModule._load = function (request) { // Request is the incoming path
const filename = MyModule._resolveFilename(request);
// Check the cache first. If the cache exists and has been loaded, return the cache directly
const cachedModule = MyModule._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// If the cache does not exist, we load the module
const module = new MyModule(filename);
// Load caches the module before, so that if there are any circular references it will be returned to the cache, but the exports in the cache may not be present or complete
MyModule._cache[filename] = module;
// If the load fails, delete the corresponding cache in _cache. I'm not going to do that for simplicity
module.load(filename);
return module.exports;
}
Copy the code
Mymodule._resolvefilename and myModule.prototype. load are also called from the source code.
MyModule._resolveFilename
The purpose of this function is to resolve the actual file address from the require parameter passed in by the user. In the source code, this method is complicated because it supports multiple parameters: built-in modules, relative paths, absolute paths, folders, and third-party modules.
For brevity, this example implements only relative file imports:
MyModule._resolveFilename = function (request) {
return path.resolve(request);
}
Copy the code
MyModule.prototype.load
Mymodule.prototype. load is an instance method and the source code is here. This method is the actual method used to load modules.
MyModule.prototype.load = function (filename) {
// Get the file name extension
const extname = path.extname(filename);
// Call the corresponding handler function of the suffix name to process. The current implementation only supports JS
MyModule._extensions[extname](this, filename);
this.loaded = true;
}
Copy the code
Load file: myModule._extensions [‘X’]
Previously mentioned methods for handling different file types are mounted on myModule._extensions. In fact, node loaders can load not only.js modules, but also.json and.node modules. For simplicity, this example only implements loading of.js files:
MyModule._extensions['.js'] = function (module, filename) {
const content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename);
}
Copy the code
As you can see, the loading method of JS is very simple, just read out the contents of the file, and then call another instance method _compile to execute it. The corresponding source code is here.
_compile implementation
Mymodule.prototype. _compile is the core of loading the JS file. This method takes out the object file and executes it. The corresponding source code is here.
_compile does the following:
Exports, require, module, __dirname, __filename. This is why we can use these variables directly in JS files. It’s not hard to do this either, if the file we require is a simple Hello World that looks like this:
module.exports = "hello world";
Copy the code
How do we inject the module variable into it? The answer is to add a layer of functions on top of it to make it look like this:
function (module) { // Inject the module variable
module.exports = "hello world";
}
Copy the code
NodeJS is also implemented in this way. In the node source code, there will be such code:
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { '.'\n}); '
];
Copy the code
The mymodule. wrap wrapper gets exports, require, module, __filename, __dirname.
2. Execute the wrapped code in the sandbox and return the module’s export. Sandbox execution uses node’s VM module.
In this implementation, the _compile implementation looks like this:
MyModule.prototype._compile = function (content, filename) {
var self = this;
// Get the wrapped function body
const wrapper = MyModule.wrap(content);
// THE VM is the NodeJS virtual machine sandbox module, and the runInThisContext method can take a string and convert it to a function
// The return value is the converted function, so compiledWrapper is a function
const compiledWrapper = vm.runInThisContext(wrapper, {
filename
});
const dirname = path.dirname(filename);
const args = [self.exports, self.require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
}
Copy the code
The wrapper and warp implementations are as follows:
MyModule.wrapper = [
'(function (myExports, myRequire, myModule, __filename, __dirname) { '.'\n}); '
];
MyModule.wrap = function (script) {
return MyModule.wrapper[0] + script + MyModule.wrapper[1];
};
Copy the code
Note that in the wrapper wrapper above we used myRequire and myModule to distinguish between the native require and module. In the following example we will use our own function to load the file.
Finally, an instance is generated and exported
Finally, we will create a new MyModule reality and export it for external use:
const myModuleInstance = new MyModule();
const MyRequire = (id) = > {
return myModuleInstance.require(id);
}
module.exports = {
MyModule,
MyRequire
}
Copy the code
The complete code
The final complete code is as follows:
const path = require('path');
const vm = require('vm');
const fs = require('fs');
function MyModule(id = ' ') {
this.id = id; // Module path
this.exports = {}; // The exported object is placed here and initialized as an empty object
this.loaded = false; // Indicates whether the current module is loaded
}
MyModule._cache = {};
MyModule._extensions = {};
MyModule.wrapper = [
'(function (myExports, myRequire, myModule, __filename, __dirname) { '.'\n}); '
];
MyModule.wrap = function (script) {
return MyModule.wrapper[0] + script + MyModule.wrapper[1];
};
MyModule.prototype.require = function (id) {
return MyModule._load(id);
}
MyModule._load = function (request) { // Request is the incoming path
const filename = MyModule._resolveFilename(request);
// Check the cache first. If the cache exists and has been loaded, return the cache directly
const cachedModule = MyModule._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// If the cache does not exist, we load the module
// Create an instance of MyModule before loading it and call the instance method load to load it
Module.exports is returned directly after loading
const module = new MyModule(filename);
// Load caches the module before, so that if there are any circular references it will be returned to the cache, but the exports in the cache may not be present or complete
MyModule._cache[filename] = module;
// If the load fails, delete the corresponding cache in _cache. I'm not going to do that for simplicity
module.load(filename);
return module.exports;
}
MyModule._resolveFilename = function (request) {
return path.resolve(request);
}
MyModule.prototype.load = function (filename) {
// Get the file name extension
const extname = path.extname(filename);
// Call the corresponding handler function of the suffix name to process. The current implementation only supports JS
MyModule._extensions[extname](this, filename);
this.loaded = true;
}
MyModule._extensions['.js'] = function (module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename);
};
MyModule.prototype._compile = function (content, filename) {
var self = this;
// Get the wrapped function body
const wrapper = MyModule.wrap(content);
// THE VM is the NodeJS virtual machine sandbox module, and the runInThisContext method can take a string and convert it to a function
// The return value is the converted function, so compiledWrapper is a function
const compiledWrapper = vm.runInThisContext(wrapper, {
filename
});
const dirname = path.dirname(filename);
const args = [self.exports, self.require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
}
const myModuleInstance = new MyModule();
const MyRequire = (id) = > {
return myModuleInstance.require(id);
}
module.exports = {
MyModule,
MyRequire
}
Copy the code
Off-topic: How is require implemented in the source code?
Nodejs v4.x, which implements require, also uses the require function in lib/module.js.
This seems to create a chicken-and-egg paradox, how can you use it before I’ve made you?
In fact, there is another simple implementation of require in the source code, which is defined in SRC/Node.js, and the source code is here.
Load files with custom MyModule
We’ve just implemented a simple Module, but it’s questionable if it works. We use our MyModule to load the file and see if it works.
You can check out demos/01, and the entry to the code is app.js:
const { MyRequire } = require('./myModule.js');
MyRequire('./b.js');
Copy the code
The code of B. js is as follows:
const { obj, setVal } = myRequire('./a.js')
console.log(obj);
setVal(101);
console.log(obj);
Copy the code
You can see that we now load the./a.js module with myRequire instead of require.
Let’s look at the code for./a.js:
let obj = {
val: 1
};
const setVal = (newVal) = > {
obj.val = newVal
}
myModule.exports = {
obj,
setVal
}
Copy the code
You can see that we now export modules with myModule instead of Module.
Finally, run node app.js to check the result:
{ val: 1 }
{ val: 101 }
Copy the code
You can see that the end result is the same as using the native Module module.
Test circular references with custom MyModule
Before we do that, let’s look at what happens to a circular reference to the native Module module. Check out demos/02, and the entry to the code is app.js:
require('./a.js')
Copy the code
Look at the code for./a.js:
const { b, setB } = require('./b.js');
console.log('running a.js');
console.log('b val', b);
console.log('setB to bb');
setB('bb')
let a = 'a';
const setA = (newA) = > {
a = newA;
}
module.exports = {
a,
setA
}
Copy the code
/b. Js:
const { a, setA } = require('./a.js');
console.log('running b.js');
console.log('a val', a);
console.log('setA to aa');
setA('aa')
let b = 'b';
const setB = (newB) = > {
b = newB;
}
module.exports = {
b,
setB
}
Copy the code
You can see that./a.js and./b.js refer to each other at the beginning of the file.
Run node app.js to view the result:
running b.js
a val undefined
setA to aa
/Users/xxx/Desktop/esm_commonjs/demos/02/b.js:9
setA('aa')
^
TypeError: setA is not a function
at Object.<anonymous> (/Users/xxx/Desktop/esm_commonjs/demos/02/b.js:9:1)
at xxx
Copy the code
We will find a TypeError error saying setA is not a function. This exception is expected, we will try our implementation of myModule exceptions and native Module behavior is consistent.
Let’s look at Demos /03, where we reproduce the circular reference above with our myModule, and the entry to the code is app.js:
const { MyRequire } = require('./myModule.js');
MyRequire('./a.js');
Copy the code
The code of A. js is as follows:
const { b, setB } = myRequire('./b.js');
console.log('running a.js');
console.log('b val', b);
console.log('setB to bb');
setB('bb')
let a = 'a';
const setA = (newA) = > {
a = newA;
}
myModule.exports = {
a,
setA
}
Copy the code
/b. Js:
const { a, setA } = myRequire('./a.js');
console.log('running b.js');
console.log('a val', a);
console.log('setA to aa');
setA('aa')
let b = 'b';
const setB = (newB) = > {
b = newB;
}
myModule.exports = {
b,
setB
}
Copy the code
You can see that we’re now replacing require with myRequire and module with myModule.
Finally, run node app.js to check the result:
running b.js
a val undefined
setA to aa
/Users/xxx/Desktop/esm_commonjs/demos/03/b.js:9
setA('aa')
^
TypeError: setA is not a function
at Object.<anonymous> (/Users/xxx/Desktop/esm_commonjs/demos/03/b.js:9:1)
at xxx
Copy the code
As you can see, myModule behaves in the same way that the native Module handles loop-referenced exceptions.
Question: How come CommonJS references to each other don’t cause “deadlock” issues?
We can see that CommonJS modules reference each other without any deadlock-like problems. The key is in the module. _load function, the specific source code is here. The module. _load function does the following:
- Check the cache. If the cache exists and has been loaded, return to the cache without doing the following
- If the cache does not exist, create a new Module instance
- Put the Module instance in the cache
- Load files through the Module instance
- Returns the exports of this Module instance
The key is the order in which files are placed in the cache and loaded, in our MyModule, these two lines of code:
MyModule._cache[filename] = module;
module.load(filename);
Copy the code
Going back to the looping example above, explain what happened:
When app.js loads a.js, the Module checks the cache for a.js, finds no a.js, creates a new A.js Module, places it in the cache, and loads the a.js file itself.
When loading the file a.js, the Module finds that the first line is loading b.js. It checks for b.js in the cache and finds no B.js, so it creates a new B.js Module, places the Module in the cache, and loads the file itself.
When loading the b.js file, Module finds that the first line is loading A. js. It checks for a.js in the cache and finds that a.js exists. Require returns a.js in the cache.
Module. exports has not yet been implemented, so b.js requires (‘./a.js’) returns a default empty object. SetA is not a function.
Speaking of which, how can design cause a “deadlock”? In fact, it is quite simple to swap the execution order of the loading file with that of the loading file. In our MyModule code, we say:
module.load(filename);
MyModule._cache[filename] = module;
Copy the code
Switch this and execute Demo03, we find the following exception:
RangeError: Maximum call stack size exceeded
at console.value (node:internal/console/constructor:290:13)
at console.log (node:internal/console/constructor:360:26)
Copy the code
We found that this would cause deadlocks and eventually result in JS stack overflow exceptions.
The execution of JavaScript
Next, we will explain module import of ESM. In order to understand ESM module import easily, we need to add a knowledge point here — JavaScript execution process.
JavaScript execution is divided into two phases:
- Compilation phase
- Execution phase
Compilation phase
At compile time, the JS engine does three main things:
- Lexical analysis
- Syntax analysis
- Bytecode generation
The details of these three things are not covered here, but interested readers can read the repository of the -super-tiny-Compiler, which implements a microcompiler with hundreds of lines of code and details the three processes in detail.
Execution phase
During the execution phase, various types of execution contexts are created on a case-by-case basis, for example: global execution context (only one), function execution context. The creation of an execution context is divided into two phases:
- Create a stage
- Execution phase
During the creation phase, the following things are done:
- Binding this
- Allocate memory for functions and variables
- Initialize the related variable to undefined
Variable promotions and function promotions are done during the creation phase, so this is not an error:
console.log(msg);
add(1.2)
var msg = 'hello'
function add(a,b){
return a + b;
}
Copy the code
Because the memory space for MSG and Add is already allocated during the creation phase before execution.
Common error types in JavaScript
To make it easier to understand module imports in ESM, I’ll add one more thing here — common JavaScript error types.
1, RangeError
This type of error is very common, for example stack overflow is a RangeError;
function a () {
b()
}
function b () {
a()
}
a()
// out:
// RangeError: Maximum call stack size exceeded
Copy the code
2, ReferenceError
ReferenceError is also common, printing a value that does not exist is ReferenceError:
hello
// out:
// ReferenceError: hello is not defined
Copy the code
3, SyntaxError
SyntaxError is also common when the syntax does not conform to the JS specification:
console.log(1));
// out:
// console.log(1));
/ / ^
// SyntaxError: Unexpected token ')'
Copy the code
4, TypeError
TypeError is also common, and is reported when an underlying type is called as a function:
var a = 1;
a()
// out:
// TypeError: a is not a function
Copy the code
Of the above Error types, SyntaxError is the most special because it is thrown at compile time. If a SyntaxError occurs, no line of JS code will be executed. Other types of exceptions are errors at the execution stage. Even if an error is reported, the script before the exception is executed.
What do you meanCompile-time output interface
? What do you meanRun time loading
?
The ESM is called a compile-time output interface because its module resolution takes place at compile time.
That is, the keywords import and export are parsed at compile time, and syntax errors will be thrown at compile time if the use of these keywords does not conform to the syntax specification.
For example, according to the ES6 specification, import can only be declared at the top of the module, so the following syntax will report a syntax error and no log will be printed because it is not executed at all:
console.log('hello world');
if (true) {
import { resolve } from 'path';
}
// out:
// import { resolve } from 'path';
/ / ^
// SyntaxError: Unexpected token '{'
Copy the code
In contrast to CommonJS, module resolution takes place at execution time because require and Module are essentially functions or objects that are instantiated only when run at execution time. Hence it is called runtime loading.
It is important to note that unlike CommonJS, import in ESM is not an object and export is not an object. For example, the following syntax error may occur:
// Syntax error! This is not deconstruction!!
import { a: myA } from './a.mjs'
// Syntax error!
export {
a: "a"
}
Copy the code
Import and export are used much like importing or exporting an object, but have nothing to do with objects at all. Their usage is designed at the ECMAScript language level, and the use of objects that “happen” to be similar.
So at compile time, values introduced in the import module point to values exported from export. For those who know Linux, this is a bit like a hard link in Linux, pointing to the same inode. Or to use the stack and heap analogy, it’s like having two Pointers to the same stack.
Loading details of the ESM
Before going into the loading details of ESM, it is important to realize that variable promotion and function promotion also exist in ESM.
Take the circular reference mentioned earlier in Demos /02, and change it to the ESM version of the circular reference. Check out Demos /04, and the entry to the code is app.js:
import './a.mjs';
Copy the code
Take a look at the code for./ a.js:
import { b, setB } from './b.mjs';
console.log('running a.mjs');
console.log('b val', b);
console.log('setB to bb');
setB('bb')
let a = 'a';
const setA = (newA) = > {
a = newA;
}
export {
a,
setA
}
Copy the code
Take a look at the code for./ b.js:
import { a, setA } from './a.mjs';
console.log('running b.mjs');
console.log('a val', a);
console.log('setA to aa');
setA('aa')
let b = 'b';
const setB = (newB) = > {
b = newB;
}
export {
b,
setB
}
Copy the code
You can see that./a.mjs and./b.mjs refer to each other at the beginning of the file.
Run node app.mjs to view the result:
running b.mjs
file:///Users/xxx/Desktop/esm_commonjs/demos/04/b.mjs:5
console.log('a val', a);
^
ReferenceError: Cannot access 'a' before initialization
at file:///Users/xxx/Desktop/esm_commonjs/demos/04/b.mjs:5:22
Copy the code
We’ll find a ReferenceError indicating that a variable cannot be used before initialization. This is because we used let to define variables and const to define functions, making it impossible to do variable and function promotion.
How do I modify it to work? It’s actually quite simple: var instead of let, use function to define functions. Let’s look at demos/05 to see what it looks like:
Take a look at the code for./ a.js:
console.log('b val', b);
console.log('setB to bb');
setB('bb')
var a = 'a';
function setA(newA) {
a = newA;
}
export {
a,
setA
}
Copy the code
Take a look at the code for./ b.js:
import { a, setA } from './a.mjs';
console.log('running b.mjs');
console.log('a val', a);
console.log('setA to aa');
setA('aa')
var b = 'b';
function setB(newB) {
b = newB;
}
export {
b,
setB
}
Copy the code
Run node app.mjs to view the result:
running b.mjs
a val undefined
setA to aa
running a.mjs
b val b
setB to bb
Copy the code
You can find that the modification can be executed normally without exceptions.
The CommonJS module. _load function does something similar to the CommonJS module. _load function:
- Check the cache. If the cache exists and has been loaded, extract the value directly from the cache module without doing the following
- If the cache does not exist, create a new Module instance
- Put the Module instance in the cache
- Load files through the Module instance
- After loading the file into the global execution context, there are create phases and execution phases, where functions and variables are promoted, followed by code execution.
- Returns the exports of this Module instance
In combination with the cyclic loading of Demos /05, let’s do a more detailed explanation:
When app.mjs loads the A.M.JS, the Module checks the cache for a.M.JS, finds none, creates an A.M.JS Module, places the Module in the cache, and loads the A.M.JS file itself.
When loading the A.m.JS file, memory space is allocated for the function setA and variable A in the global context during the creation phase, and variable A is initialized to undefined. In the execution phase, it finds that the first line is loading b.js. It checks if there is b.js in the cache, finds no B.js, so it creates a new B.js module, puts the module in the cache, and loads the B.js file itself.
When loading the B.js file, memory space is allocated for function setB and variable B in the global context during the creation phase, and variable B is initialized as undefined. In the execution phase, the first line is to load the A.M.JS, which checks for a.M.JS in the cache and finds that it exists, so the import returns the corresponding value of the A.M.JS exported from the cache.
Although a.mjs has not been executed at this time, its creation phase has already been completed, that is, the setA function and the variable a with the value of undefined already exist in memory. So in B.js you can print a normally and use the setA function without throwing exceptions.
Again, the difference between ESM and CommonJS
Different points: This points differently
CommonJS this reference can be viewed source code:
var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
Copy the code
This clearly points to the default exports of the current Module;
The ESM points to undefined because of its linguistic design.
Differences: __filename, __dirname exist in CommonJS, not in ESM
In CommonJS, the execution of a module needs to be wrapped in functions that specify common values.
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { '.'\n}); '
];
Copy the code
That’s why we can use __filename and __dirname globally. The ESM does not have this design, so __filename and __dirname cannot be used directly in the ESM.
Similarities: Both ESM and CommonJS have caches
In this point, the two module schemes are the same. Both modules will be cached. Once a module is loaded, it will be cached, and the module in the cache will be used for subsequent loading.
Reference documentation
- Ruan Yifeng: Module loading implementation
- Drill into node.js’s module loading mechanism by writing the require function
- Commonjs vs. ESM
- The Node.js Way – How
require()
Actually Works - stackoverflow:How does require() in node.js work?
- Node module loading mechanism: shows some scenarios for modifying require
- Docs: Differences between the ES module and CommonJS
- Requiring modules in Node.js: Everything you need to know
- JavaScript Execution Context and Hoisting Explained with Code Examples
- An in-depth look at JavaScript execution (one of the JS series)
- JS execution process details
- 7 Types of Native Errors in JavaScript You Should Know