Ruan Yifeng is in ES6 introduction I mentioned ES6 Module and CommonJS There are some major differences in the modules :
- CommonJS The output of the module is a copy of the value ,ES6 The module outputs a reference to a value .
- CommonJS Modules are run time loaded ,ES6 Module is a compile time output interface .
Read the differences mentioned by teacher Ruan , There will be many questions :
- Why? CommonJS The output of the module is a copy of the value ? What are the specific details ?
- What do you mean
Load at run time
? - What do you mean
Compile time output interface
? - Why? ES6 The module outputs a reference to a value ?
Hence the article , Strive to ESM modular and CommonJS modular Discuss clearly .
CommonJS The historical background of its emergence
CommonJS from Mozilla The engineer Kevin Dangoor On 2009 year 1 Founded in , It was originally named ServerJS.2009 year 8 month , The project was renamed as CommonJS. To solve Javascript The lack of modular standards in .
Node.js Later, it was also adopted CommonJS Module specification of .
because CommonJS Not at all ECMAScript Part of the standard , therefore similar module
and require
Not at all JS Key words of , Just objects or functions , It's important to be aware of this .
We can print module
、require
Check the details :
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]
}
}
}
You can see module
It's an object , require
It's a function , That's it .
Let's focus on module
Some properties in :
exports
: This is it.module.exports
Corresponding value , Since no value has been assigned to it , It is currently an empty object .loaded
: Indicates whether the current module is loaded .paths
:node Loading path of module , Don't talk about this , You can see if you are interested node file
require
There are also some noteworthy properties in the function :
main
Point to the module that currently references itself , So similar python Of__name__ == '__main__'
, node It can also be used.require.main === module
To determine whether the program is started with the current module .extensions
At present node Support several ways of loading modules .cache
Express node Cache loaded by the module in , in other words , When a module is loaded once , afterrequire
It will not be loaded again , Instead, read from the cache .
Mentioned earlier ,CommonJS in module
It's an object , require
It's a function . And the corresponding ESM Medium import
and export
Is the keyword , yes ECMAScript Part of the standard . Understanding the difference between the two is crucial .
Look at some first CommonJS Example
Let's take a look at the following CommonJS Example , See if you can accurately predict the results :
Patients with a , Assign values to simple types 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);
function b.js
, The output is :
1
1
Example 2 , Assign a value to the 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);
function b.js
, The output is :
{ val: 1 }
{ val: 101 }
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)
function b.js
, The output is :
1
1
Example 4 , Use after exporting in the module module.exports
Export again :
// 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)
function b.js
, The output is :
{ val: 1 }
{ val: 1 }
Example 5 , Use after exporting in the module exports
Export again :
// 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)
function b.js
, The output is :
{ val: 1 }
{ val: 101 }
How to explain the above example ? There is no magic ! A word tells the truth CommonJS Details of value copy
take out JS The most simple thinking , To analyze the above examples of various phenomena .
In example 1 , The code can be reduced 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);
Example 2 , The code can be reduced to :
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);
In example 3 , The code can be reduced 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)
In example 4 , The code can be reduced to :
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)
In example 5 , The code can be reduced to :
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)
Try running the above code , Can be found and CommonJS The output effect is consistent . therefore CommonJS It's not magic , It's just the simplest thing to write everyday JS Code .
The copy of its value occurs to module.exports
The moment of assignment , for example :
let val = 1;
module.exports = {
val
}
What you do is just give module.exports
Given a new object , There is one in this object key be called val
, This val
The value of is in the current module val
Value , That's it .
CommonJS The concrete realization of
For a better understanding of CommonJS, Let's write a simple module loader , It mainly refers to nodejs Source code ;
stay node v16.x in module Mainly realized in lib/internal/modules/cjs/loader.js
file Next .
stay node v4.x in module Mainly realized in lib/module.js
file Next .
The following implementation mainly refers to node v4.x In the implementation of , Because the old version is relatively more “ clean ” some , It's easier to grasp the details .
in addition thorough Node.js Module loading mechanism , Handwriting require function This article is also very good , Many of the following implementations also refer to this article .
In order to communicate with the authorities Module Separate names , Our own class is named MyModule:
function MyModule(id = '') {
this.id = id; // The module path
this.exports = {}; // Export things here , Initialize to empty object
this.loaded = false; // Used to identify whether the current module has been loaded
}
require Method
We always use it require
It's actually Module An instance method of class , The content is very simple , First, check some parameters , And then call Module._load Method , The source code in here , This example is for brevity , Some judgments have been removed :
MyModule.prototype.require = function (id) {
return MyModule._load(id);
}
require
Is a very simple function , Mainly packaging _load
function , This function mainly does the following things :
- First check whether the requested module already exists in the cache , If there is a direct return to the cache module
exports
- If not in cache , Just create one
Module
example , Put the instance in the cache , Use this instance to load the corresponding module , And return the of the moduleexports
MyModule._load = function (request) { // request Is the incoming path
const filename = MyModule._resolveFilename(request);
// First check the cache , If the cache exists and is already loaded , Directly back to cache
const cachedModule = MyModule._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// If the cache does not exist , Let's load this module
const module = new MyModule(filename);
// load Cache this module before , In this way, if there is a circular reference, you will get this cache , But in this cache exports May not be or incomplete
MyModule._cache[filename] = module;
// If load Failure , Need to put _cache Delete the corresponding cache in . For the sake of simplicity , Don't do this
module.load(filename);
return module.exports;
}
You can see that the above source code also calls two methods :MyModule._resolveFilename
and MyModule.prototype.load
, Let's implement these two methods .
MyModule._resolveFilename
This function is passed in by the user require Parameters to resolve to the real file address , Source code This method is more complex , Because it needs to support multiple parameters : Built-in module , Relative paths , Absolute path , Folders and third-party modules, etc .
This example is for brevity , Only import relative files :
MyModule._resolveFilename = function (request) {
return path.resolve(request);
}
MyModule.prototype.load
MyModule.prototype.load
Is an example method , The source code in here , This method is really used to load modules , This is actually an entry for loading different types of files , Different types of files will correspond to MyModule._extensions
One of the ways is :
MyModule.prototype.load = function (filename) {
// Get file suffix
const extname = path.extname(filename);
// Call the processing function corresponding to the suffix to handle , The current implementation only supports JS
MyModule._extensions[extname](this, filename);
this.loaded = true;
}
Load the file : MyModule._extensions['X']
The processing methods of different file types mentioned above are all mounted in MyModule._extensions
On , in fact node
The loader of can not only load .js
modular , You can also load .json
and .node
modular . For simplicity, this example only implements .js
Loading of type files :
MyModule._extensions['.js'] = function (module, filename) {
const content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename);
}
You can see js The loading method of is very simple , Just read out the contents of the document , Then another instance method is called _compile
To execute him . The corresponding source code is here .
_compile Realization
MyModule.prototype._compile
Is load JS The core of the document , This method needs to take out the object file and execute it . The corresponding source code is here .
_compile
I mainly did the following things :
1、 Before execution, you need to wrap the whole code , To inject exports
, require
, module
, __dirname
, __filename
, This is also what we can do in JS The reason why these variables are directly used in the file . It is not difficult to achieve this kind of injection , If we require The file is a simple Hello World
, Long like this :
module.exports = "hello world";
Then how can we inject him module
This variable ? The answer is to add another layer of functions outside him when executing , Make him like this :
function (module) { // Inject module Variable , In fact, several variables are the same
module.exports = "hello world";
}
nodeJS That's how it works , stay node Source code in , There will be code like this :
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
This way MyModule.wrap The packaged code can be obtained exports
, require
, module
, __filename
, __dirname
These variables .
2、 Put it into the sandbox and execute the packaged code , And return the of the module export. Sandbox execution uses node Of vm
modular .
In this implementation ,_compile
The implementation is as follows :
MyModule.prototype._compile = function (content, filename) {
var self = this;
// Get the wrapped function body
const wrapper = MyModule.wrap(content);
// vm yes nodejs Virtual machine sandbox module ,runInThisContext Method can accept a string and convert it into a function
// The return value is the converted function , therefore compiledWrapper It's 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);
}
wrapper
and warp
The implementation is as follows :
MyModule.wrapper = [
'(function (myExports, myRequire, myModule, __filename, __dirname) { ',
'\n});'
];
MyModule.wrap = function (script) {
return MyModule.wrapper[0] + script + MyModule.wrapper[1];
};
Notice the topwrapper
And we used thatmyRequire
andmyModule
To distinguish nativerequire
andmodule
, In the following example, we will use our own functions to load files .
Finally, generate an instance and Export
Finally we new One MyModule
And derive , Convenient for external use :
const myModuleInstance = new MyModule();
const MyRequire = (id) => {
return myModuleInstance.require(id);
}
module.exports = {
MyModule,
MyRequire
}
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; // The module path
this.exports = {}; // Export things here , Initialize to empty object
this.loaded = false; // Used to identify whether the current module has been 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);
// First check the cache , If the cache exists and is already loaded , Directly back to cache
const cachedModule = MyModule._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// If the cache does not exist , Let's load this module
// Before loading new One MyModule example , Then call the instance method. load To load the
// After loading, return directly module.exports
const module = new MyModule(filename);
// load Cache this module before , In this way, if there is a circular reference, you will get this cache , But in this cache exports May not be or incomplete
MyModule._cache[filename] = module;
// If load Failure , Need to put _cache Delete the corresponding cache in . For the sake of simplicity , Don't do this
module.load(filename);
return module.exports;
}
MyModule._resolveFilename = function (request) {
return path.resolve(request);
}
MyModule.prototype.load = function (filename) {
// Get file suffix
const extname = path.extname(filename);
// Call the processing function corresponding to the suffix to handle , 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);
// vm yes nodejs Virtual machine sandbox module ,runInThisContext Method can accept a string and convert it into a function
// The return value is the converted function , therefore compiledWrapper It's 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
}
Digression : In source code require How is it realized ?
Careful readers will find out : nodejs v4.x Source code to achieve require
Of file lib/module.js
in , It also uses require
function .
This seems to produce the paradox of whether there is a chicken or an egg first , I haven't made you yet , How can you use it ?
in fact , In the source require
There is another simple implementation , It is defined in src/node.js
in , The source code in here ).
Use custom MyModule To load the file
We just implemented a simple Module, But whether it can be used normally is still in doubt . The mule or the horse came out for a walk , We use our own MyModule
To load the file , Let's see if it works .
You can see demos/01
, The entry to the code is app.js
:
const { MyRequire } = require('./myModule.js');
MyRequire('./b.js');
b.js
The code for is as follows :
const { obj, setVal } = myRequire('./a.js')
console.log(obj);
setVal(101);
console.log(obj);
You can see that now we use myRequire
replace require
To load the ./a.js
modular .
I want to see others ./a.js
Code for :
let obj = {
val: 1
};
const setVal = (newVal) => {
obj.val = newVal
}
myModule.exports = {
obj,
setVal
}
You can see that now we use myModule
replace module
To export modules .
Finally, execute node app.js
View the run results :
{ val: 1 }
{ val: 101 }
You can see the final effect and use native module Modules are consistent .
Use custom MyModule To test circular references
before this , Let's first look at the native module What exceptions will happen to the circular reference of the module . You can see demos/02
, The entry to the code is app.js
:
require('./a.js')
have a look ./a.js
Code for :
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
}
I want to see others ./b.js
Code for :
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
}
You can see ./a.js
and ./b.js
They all refer to each other at the beginning of the document .
perform node app.js
View the run results :
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
We'll find one TypeError An error is reported for the exception , Tips setA is not a function
. Such an anomaly is expected , Let's try our own myModule
Whether the exception of is native module
Act in unison .
We see the demos/03
, Here we use our own myModule
To reproduce the above circular reference , The entry to the code is app.js
:
const { MyRequire } = require('./myModule.js');
MyRequire('./a.js');
a.js
The code for 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
}
I want to see others ./b.js
Code for :
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
}
You can see that now we use myRequire
To replace the require
, use myModule
To replace the module
.
Finally, execute node app.js
View the run results :
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
You can see ,myModule
Behavior and primordial Module
It is consistent to handle exceptions of circular references .
doubt : Why? CommonJS Cross references do not produce similar “ Deadlock ” The problem of ?
We can find out CommonJS When modules reference each other , There is no deadlock like problem . The key is Module._load
In the function , The specific source code is in here .Module._load
The function mainly does the following things :
- Check cache , If the cache exists and is already loaded , Directly back to cache , Do not do the following processing
- If the cache does not exist , Create a new one Module example
- Put this Module The instance is put into the cache
- Through this Module Instance to load the file
- Go back to this Module Example of exports
The key is Put it in the cache And Load the file The order of , In our MyModule
in , That's two lines of code :
MyModule._cache[filename] = module;
module.load(filename);
Back to the example of cyclic loading above , Explain what happened :
When app.js
load a.js
when ,Module It will check whether there is a.js
, Found no , therefore new One a.js
modular , And put this module into the cache , Reload a.js
The document itself .
In the load a.js
When you file ,Module The first line is loading b.js
, It will check whether there is b.js
, Found no , therefore new One b.js
modular , And put this module into the cache , Reload b.js
The document itself .
In the load b.js
When you file ,Module The first line is loading a.js
, It will check whether there is a.js
, found , therefore require
Function returns... In the cache a.js
.
But actually this time a.js
It's not finished yet , Not yet module.exports
That step , therefore b.js
in require('./a.js')
Only a default empty object is returned . So I will finally report setA is not a function
It's abnormal .
Speaking of this , Then how to design will lead to “ Deadlock ” Well ? It's also very simple —— take Put it in the cache And Load the file The execution sequence of is interchanged , In our MyModule
In the code , That's what it says :
module.load(filename);
MyModule._cache[filename] = module;
Exchange it like this , Re execution demo03, We found the following anomalies :
RangeError: Maximum call stack size exceeded
at console.value (node:internal/console/constructor:290:13)
at console.log (node:internal/console/constructor:360:26)
We found that writing like this would deadlock , Eventually lead to JS Stack overflow exception .
JavaScript Implementation process of
Next we will explain ESM Module import , For ease of understanding ESM Module import , Here we need to add a knowledge point —— JavaScript Implementation process of .
JavaScript The execution process is divided into two stages :
- Compile phase
- Execution phase
Compile phase
In the compilation phase JS The engine mainly does three things :
- Lexical analysis
- Syntax analysis
- Bytecode generation
There are no details about these three things , Interested readers can read the-super-tiny-compiler This warehouse , It implements a microform compiler through hundreds of lines of code , And the specific details of these three processes are described in detail .
Execution phase
In the execution phase , Various types of execution contexts will be created according to the situation , for example : Global execution context ( only one )、 Function execution context . The creation of execution context is divided into two stages :
- Create a stage
- Execution phase
During the creation phase, the following things will be done :
- binding this
- Allocate memory space for functions and variables
- The initialization related variables are undefined
We mentioned everyday Variable Promotion and Function enhancement Is in the Create a stage It's done , Therefore, the following wording will not report an error :
console.log(msg);
add(1,2)
var msg = 'hello'
function add(a,b){
return a + b;
}
Because in the creation phase before execution , It's been allocated msg
and add
Of memory space .
JavaScript Common error types
To make it easier to understand ESM Module import , Here is another knowledge point —— JavaScript Common error types .
1、RangeError
Such errors are common , For example, stack overflow is RangeError
;
function a () {
b()
}
function b () {
a()
}
a()
// out:
// RangeError: Maximum call stack size exceeded
2、ReferenceError
ReferenceError
Is also common , Printing a non-existent value is ReferenceError
:
hello
// out:
// ReferenceError: hello is not defined
3、SyntaxError
SyntaxError
Is also common , When grammar does not conform JS When standardizing , Will report this error :
console.log(1));
// out:
// console.log(1));
// ^
// SyntaxError: Unexpected token ')'
4、TypeError
TypeError
Is also common , When a basic type is used as a function , You will report this error :
var a = 1;
a()
// out:
// TypeError: a is not a function
All kinds of above Error Type in the ,SyntaxError
The most special , Because it is Compile phase The mistakes thrown out , If a syntax error occurs ,JS No line of code will be executed . Other types of exceptions are Execution phase Error of , Even if you report an error , The script before the exception will also be executed .
What do you mean Compile time output interface
? What do you mean Load at run time
?
ESM It's called Compile time output interface
, Because its module parsing occurs in Compile phase .
in other words ,import
and export
These keywords are parsed in the compilation stage , If the use of these keywords does not conform to the syntax , Syntax errors will be thrown during compilation .
for example , according to ES6 standard ,import
Can only be declared at the top level of the module , Therefore, the following writing method will directly report grammatical errors , There will be no log Print , Because it didn't enter at all Execution phase :
console.log('hello world');
if (true) {
import { resolve } from 'path';
}
// out:
// import { resolve } from 'path';
// ^
// SyntaxError: Unexpected token '{'
Corresponding to this CommonJS, Its module parsing takes place in Execution phase , because require
and module
It is essentially a function or object , Only in Execution phase Runtime , These functions or objects are instantiated . So it's called Load at run time
.
I want to emphasize that , And CommonJS Different ,ESM in import
Is not an object , export
Is not an object . for example , The following writing will prompt grammatical errors :
// Grammar mistakes ! This is not deconstruction !!!
import { a: myA } from './a.mjs'
// Grammar mistakes !
export {
a: "a"
}
import
and export
The usage of is much like importing an object or exporting an object , But it has nothing to do with the object . Their usage is ECMAScript Language level design , also “ Happen to happen ” The use of objects is similar .
So in the compilation phase ,import
The value introduced in the module points to export
Values exported in . If the reader understands linux, It's kind of like linux Hard links in , Point to the same inode. Or take stack and heap as an analogy , It's like two pointers pointing to the same stack .
ESM Loading details
In the interpretation of the ESM Before loading details , We need to understand ESM There are also Variable Promotion and Function enhancement , It's very important to realize that .
Take the front demos/02
Examples of circular references mentioned in , Transform it into ESM Circular reference of version , see demos/04
, The entry to the code is app.js
:
import './a.mjs';
have a look ./a.mjs
Code for :
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
}
I want to see others ./b.mjs
Code for :
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
}
You can see ./a.mjs
and ./b.mjs
They all refer to each other at the beginning of the document .
perform node app.mjs
View the run results :
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
We'll find one ReferenceError An error is reported for the exception , Hint you cannot use variables before initialization . This is because we used let
Defining variables , Used const
Defined function , As a result, it is impossible to promote variables and functions .
How to modify it to work properly ? It's very simple : use var
Instead of let
, Use function To define a function , We see the demos/05
Look at the effect :
have a look ./a.mjs
Code for :
console.log('b val', b);
console.log('setB to bb');
setB('bb')
var a = 'a';
function setA(newA) {
a = newA;
}
export {
a,
setA
}
I want to see others ./b.mjs
Code for :
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
}
perform node app.mjs
View the run results :
running b.mjs
a val undefined
setA to aa
running a.mjs
b val b
setB to bb
It can be found that this modification can be executed normally , There is no abnormal error .
Here we can talk in detail ESM Loading details of , It is actually the same as the one mentioned above CommonJS Of Module._load
Functions do something similar :
- Check cache , If the cache exists and is already loaded , Then extract the corresponding value directly from the cache module , Do not do the following processing
- If the cache does not exist , Create a new one Module example
- Put this Module The instance is put into the cache
- Through this Module Instance to load the file
- After loading the file, go to Global execution context when , There will be a creation phase and an execution phase , Do function and variable promotion in the creation stage , Then execute the code .
- Go back to this Module Example of exports
combination demos/05
The cyclic loading of , Let's make a detailed explanation :
When app.mjs
load a.mjs
when ,Module It will check whether there is a.mjs
, Found no , therefore new One a.mjs
modular , And put this module into the cache , Reload a.mjs
The document itself .
In the load a.mjs
When you file , stay Create a stage Will be a function in the global context setA
and Variable a
Allocate memory space , And initialize the variable a
by undefined
. In the execution phase , The first line is loading b.mjs
, It will check whether there is b.mjs
, Found no , therefore new One b.mjs
modular , And put this module into the cache , Reload b.mjs
The document itself .
In the load b.mjs
When you file , stay Create a stage Will be a function in the global context setB
and Variable b
Allocate memory space , And initialize the variable b
by undefined
. In the execution phase , The first line is loading a.mjs
, It will check whether there is a.mjs
, found , therefore import
Returned to cache a.mjs
The corresponding value exported .
Although this time a.mjs
It has not been implemented at all , But it's Create a stage It's done , It already exists in memory setA
The function and value are undefined
The variable of a
. So at this time b.mjs
It can be printed normally a
And use setA
Function without exception throw error .
We can talk ESM and CommonJS The difference between
Difference :this The direction of is different
CommonJS Of this Point to view Source code :
var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
It's clear that this
Point to the present module
Default exports
;
and ESM Because the design at the language level points to undefined
.
Difference :__filename,__dirname stay CommonJS in , stay ESM Does not exist in the
stay CommonJS in , The execution of the module needs to be wrapped with functions , And specify some common values , You can see Source code :
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
So we can directly use the overall situation __filename
、__dirname
. and ESM There is no such design , So in ESM You can't use __filename
and __dirname
.
The same thing :ESM and CommonJS There are caches
The two module schemes are consistent , Will cache modules , After the module is loaded once, it will be cached , Subsequent reloading will use the modules in the cache .
Reference documents
- Ruan Yifeng :Module Load implementation
- thorough Node.js Module loading mechanism , Handwriting require function
- commonjs And esm The difference between
- The Node.js Way - How
require()
Actually Works - stackoverflow:How does require() in node.js work?
- Node Module loading mechanism : Show some magic changes require Scene
- docs: ES Module and CommonJS Differences between
- Requiring modules in Node.js: Everything you need to know
- JavaScript Execution Context and Hoisting Explained with Code Examples
- Deepen understanding JavaScript Execution process (JS One of series )
- JS Detailed explanation of the implementation process
- 7 Types of Native Errors in JavaScript You Should Know