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 meanRun time loading?
  • What do you meanCompile-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.exportsThe 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:

  • mainRefers to the current current reference to its own module, so similar to Python__name__ == '__main__', node can also be usedrequire.main === moduleTo determine whether the program is started with the current module.
  • extensionsRepresents several modes of loading modules currently supported by Node.
  • cacheRepresents the cache of module loads in Node, i.e., when a module is loaded once, afterrequireInstead 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 directlyexports
  • If it is not in the cache, create oneModuleInstance, 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:

  1. Check the cache. If the cache exists and has been loaded, return to the cache without doing the following
  2. If the cache does not exist, create a new Module instance
  3. Put the Module instance in the cache
  4. Load files through the Module instance
  5. 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:

  1. Check the cache. If the cache exists and has been loaded, extract the value directly from the cache module without doing the following
  2. If the cache does not exist, create a new Module instance
  3. Put the Module instance in the cache
  4. Load files through the Module instance
  5. 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.
  6. 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