Explain ESM module and CommonJS module in simple terms

Posted by Phate on Sun, 13 Feb 2022 02:43:48 +0100

Ruan Yifeng Getting started with ES6 There are some significant differences between ES6 module and CommonJS module mentioned in:

  • The CommonJS module outputs a copy of the value, while the ES6 module outputs a reference to the value.
  • CommonJS module is loaded at run time, and ES6 module is the output interface at compile time.

If you read the differences mentioned above carefully, you will have many questions:

  • Why does the CommonJS module output a copy of a value? What are the specific details?
  • What is runtime loading?
  • What is a compile time output interface?
  • Why does the ES6 module output a reference to a value?

Therefore, this article tries to discuss the ESM module and CommonJS module clearly.

Historical background of CommonJS

CommonJS was founded by Mozilla engineer Kevin Dangoor in January 2009 and was originally named ServerJS. In August 2009, the project was renamed CommonJS. It aims to solve the problem of lack of modular standards in Javascript.

Node.js later adopted the module specification of CommonJS.

Since CommonJS is not a part of ECMAScript standard, it is important to realize that module s and require are not keywords of JS, but just objects or functions.

We can check the details in print module and 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]
    }
  }
}

You can see that module is an object and require is a function, that's all.

Let's focus on some attributes in the module:

  • Exports: This is module The value corresponding to exports. Since no value has been assigned to it, it is currently an empty object.
  • Loaded: indicates whether the current module has been loaded.
  • paths: the loading path of the node module. This part is not discussed. You can see it if you are interested node document

There are also some noteworthy properties in the require function:

  • Main refers to the module that currently references itself, so it is similar to python__ name__ == '__main__', node can also use require Main = = = module to determine whether the program is started with the current module.
  • extensions refers to several methods of loading modules currently supported by node.
  • Cache refers to the cache loaded by modules in node, that is, after a module is loaded once, require will not load again, but read from the cache.

As mentioned earlier, in CommonJS, module is an object and require is a function. Corresponding to this, import and export in ESM are keywords, which are part of ECMAScript standard. It is crucial to understand the difference between the two.

Let's take a look at some examples of CommonJS

Let's take a look at the following CommonJS examples to see if we can accurately predict the results:

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);

Run b.js and the output result 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);

Run b.js and the output result 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)

Run b.js and the output result is:

1
1

Example 4: after exporting in the module, use 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)

Run b.js and the output result is:

{ val: 1 }
{ val: 1 }

Example 5: after exporting in the module, export again with 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)

Run b.js and the output result is:

{ val: 1 }
{ val: 101 }

How to explain the above example? No magic! One word reveals the details of the CommonJS value copy

Take out JS's most simple thinking to analyze various phenomena in the above examples.

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);

In example 2, the code can be simplified 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 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)

In example 4, the code can be simplified 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 simplified 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 to run the above code, and you can find that the effect is consistent with the output of CommonJS. So CommonJS is not magic, just the simplest JS code written everyday.

Its value copy occurs when it is given to module The moment when exports is assigned, for example:

let val = 1;
module.exports = {
  val
}

The only thing to do is to give the module Exports gives a new object. In this object, there is a key called val. the value of Val is the value of Val in the current module, that's all.

Concrete implementation of CommonJS

In order to understand CommonJS more thoroughly, let's write a simple module loader, mainly referring to the nodejs source code;

In node V16 module in X is mainly implemented in lib / internal / modules / CJS / loader js file Down.

In node v4 Module in X is mainly implemented in lib / module js file Down.

The following implementation mainly refers to node v4 X, because the old version is relatively "cleaner" and easier to grasp the details.

in addition Go deep into node JS module loading mechanism, handwritten require function This article is also very good. Many implementations below also refer to this article.

In order to distinguish from the official Module name, our own class is named MyModule:

function MyModule(id = '') {
  this.id = id;             // Module path
  this.exports = {};        // Put the exported object here and initialize it as an empty object
  this.loaded = false;      // Used to identify whether the current module has been loaded
}

require method

The require we have been using is actually an example method of Module class. The content is very simple. First we do some parameter checking, then we call Module._. Load method, source code in here , for the sake of brevity, this example removes some judgments:

MyModule.prototype.require = function (id) {
  return MyModule._load(id);
}

require is a very simple function, mainly for packaging_ load function, which mainly does the following things:

  • First check whether the requested module already exists in the cache. If there are exports that directly return to the cache module
  • If it is not in the cache, create a Module instance, put the instance in the cache, load the corresponding Module with this instance, and return the exports of the Module
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 to the cache directly
  const cachedModule = MyModule._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }

  // If the module does not exist, we will load it
  const module = new MyModule(filename);

  // The module is cached before load, so that if there is a circular reference, it will get the cache, but the exports in the cache may not be available or incomplete
  MyModule._cache[filename] = module;

  // If the load fails, you need to_ Delete the corresponding cache in the cache. For simplicity, do not 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 used to resolve to the real file address through the require parameter passed in by the user, Source code This method is complex because it supports multiple parameters: built-in module, relative path, absolute path, folder and third-party module, etc.

For the sake of brevity, this example only implements the import of relative files:

MyModule._resolveFilename = function (request) {
  return path.resolve(request);
}

MyModule.prototype.load

MyModule.prototype.load is an instance method. The source code is in here , this method is really used to load modules. In fact, it is also an entry for loading different types of files. Different types of files will correspond to mymodule_ One method in extensions:

MyModule.prototype.load = function (filename) {
  // Get file suffix
  const extname = path.extname(filename);

  // Call the processing function corresponding to the suffix to process. The current implementation only supports JS
  MyModule._extensions[extname](this, filename);

  this.loaded = true;
}

Load file: mymodule_ extensions['X']

As mentioned earlier, the processing methods of different file types are mounted in mymodule_ In fact, the node loader can not only load js module can also be loaded json and Node module. For the sake of simplicity, this example is only implemented js type file loading:

MyModule._extensions['.js'] = function (module, filename) {
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename);
}

You can see that the loading method of js is very simple, just read the contents of the file, and then adjust another instance method. compile to execute him. The corresponding source code is here.

_ compile implementation

MyModule.prototype._compile is the core of loading JS files. This method needs to take out the target file and execute it again. The corresponding source code is here.

_ compile mainly does the following:

1. Before execution, you need to wrap the whole code in one layer to inject exports, require, module__ dirname, __ Filename, which is why we can directly use these variables in JS files. It's not difficult to realize this kind of injection. If our required file is a simple Hello World, it looks like this:

module.exports = "hello world";

How can we inject the variable module into it? The answer is to add another layer of functions outside him during execution, so that he becomes like this:

function (module) { // Inject the module variable. In fact, several variables are the same
  module.exports = "hello world";
}

nodeJS is also implemented in this way node source code In, 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});'
];

Through mymodule The code wrapped in wrap can obtain exports, require, module__ filename, __ Dirname variables.

2. Put it into the sandbox, execute the wrapped code, and return to the export of the module. Sandbox execution uses node's vm module.

In this implementation_ The implementation of compile is as follows:

MyModule.prototype._compile = function (content, filename) {
  var self = this;
  // Get wrapped function body
  const wrapper = MyModule.wrap(content);

  // vm is the virtual machine sandbox module of nodejs. The runInThisContext method can accept a string and convert it into 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);
}

wrapper and warp are implemented as follows:

MyModule.wrapper = [
  '(function (myExports, myRequire, myModule, __filename, __dirname) { ',
  '\n});'
];

MyModule.wrap = function (script) {
  return MyModule.wrapper[0] + script + MyModule.wrapper[1];
};

Note that in the wrapper above, we use myRequire and myModule to distinguish between native require and module. In the following example, we will use our own functions to load files.

Finally, an instance is generated and exported

Finally, we create a new MyModule theory and export it 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;             // Module path
  this.exports = {};        // Put the exported object here and initialize it as an 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);

  // Check the cache first. If the cache exists and has been loaded, return to the cache directly
  const cachedModule = MyModule._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }

  // If the module does not exist, we will load it
  // Before loading, new a MyModule instance, then call the instance method load to load.
  // After loading, directly return to module exports
  const module = new MyModule(filename);

  // The module is cached before load, so that if there is a circular reference, it will get the cache, but the exports in the cache may not be available or incomplete
  MyModule._cache[filename] = module;

  // If the load fails, you need to_ Delete the corresponding cache in the cache. For simplicity, do not 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 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 wrapped function body
  const wrapper = MyModule.wrap(content);    

  // vm is the virtual machine sandbox module of nodejs. The runInThisContext method can accept a string and convert it into 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
}

Aside: how is the require in the source code implemented?

Careful readers will find: nodejs v4 Implementation of require in X source code file lib/module.js, the require function is also used.

This seems to lead to the paradox of whether there is a chicken or an egg first. How can you use it before I make you?

In fact, there is another simple implementation of require in the source code, which is defined in Src / node JS, the source code is in here).

Load files with custom MyModule

We doubt whether we can use a simple Module. It's a mule or a horse. We use our own MyModule to load the file and see if it can run normally.

Can view demos/01 , the code entry is app js:

const { MyRequire } = require('./myModule.js');

MyRequire('./b.js');

b.js code 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 instead of require to load/ a.js module.

Look again/ a.js code:

let obj = {
  val: 1
};

const setVal = (newVal) => {
  obj.val = newVal
}

myModule.exports = {
  obj,
  setVal
}

You can see that now we use myModule instead of module to export the module.

Finally, execute node app JS to view the running results:

{ val: 1 }
{ val: 101 }

You can see that the final effect is consistent with using the native module module.

Use the custom MyModule to test the circular reference

Before that, let's take a look at what exceptions will happen to the circular reference of the native module module. Can view demos/02 , the code entry is app js:

require('./a.js')

Look/ a.js code:

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
}

Look again/ b.js code:

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 refers to each other at the beginning of the file.

Execute node app JS to view the running 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 will find an exception of TypeError and an error will be reported, indicating setA is not a function. Such exceptions are expected. Let's try whether the exceptions of our own myModule are consistent with the behavior of the native module.

Let's see demos/03 , here we use our own myModule to reproduce the above circular reference. The code entry is app js:

const { MyRequire } = require('./myModule.js');

MyRequire('./a.js');

a.js code 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
}

Look again/ b.js code:

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
}

We can see that we have replaced require with myRequire and module with myModule.

Finally, execute node app JS to view the running 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

As you can see, the behavior of myModule is consistent with that of the native Module in handling the exception of circular reference.

Question: why doesn't CommonJS cross reference cause a problem like "deadlock"?

We can find that there is no deadlock like problem when CommonJS modules refer to each other. The key lies in the module_ In the load function, the specific source code is here . Module._ The load function mainly does the following things:

  1. Check the cache. If the cache exists and has been loaded, directly return to the cache without the following processing
  2. If the cache does not exist, create a new Module instance
  3. Put this Module instance into the cache
  4. Load the file through this Module instance
  5. Returns the exports of this Module instance

The key lies in the order of putting in the cache and loading files. In our MyModule, that is, these two lines of code:

MyModule._cache[filename] = module;
module.load(filename);

Go back to the example of cyclic loading above and explain what happened:

When JS when loading a.js, the Module will check whether there is a.js in the cache. If it finds no, it will create a new a.js Module, put the Module in the cache, and then load the a.js file itself.

When loading the a.js file, the Module finds that the first line is to load b.js. It will check whether there is b.js in the cache. If it finds no, it creates a new b.js Module, puts the Module in the cache, and then loads the b.js file itself.

When loading the b.js file, the Module finds that the first line is loading a.js. It will check whether there is a.js in the cache and find that it exists, so the require function returns the a.js in the cache.

But in fact, at this time, a.js has not been executed at all and has not reached module Exports, so the required ('. / a.js') in b.js returns only a default empty object. Therefore, the exception of setA is not a function will be reported in the end.

At this point, how can design lead to "deadlock"? In fact, it is also very simple - the execution order of putting it into the cache and loading the file is interchanged. In our MyModule code, it is written as follows:

module.load(filename);
MyModule._cache[filename] = module;

After this exchange, execute demo03, and we find the following exceptions:

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 will cause deadlock and eventually lead to JS stack overflow exception.

JavaScript execution process

Next, we will explain the module import of ESM. In order to understand the module import of ESM, we need to add a knowledge point here - the execution process of JavaScript.

The JavaScript execution process is divided into two stages:

  • Compilation phase
  • Execution phase

Compilation phase

In the compilation phase, the JS engine mainly does three things:

  • lexical analysis
  • Grammar analysis
  • Bytecode generation

The details of these three events are not discussed here. Interested readers can read them the-super-tiny-compiler This warehouse implements a micro compiler through hundreds of lines of code, and describes the specific details of these three processes in detail.

Execution phase

In the execution phase, various types of execution contexts will be created according to different situations, such as global execution context (only one) and function execution context. The creation of execution context is divided into two stages:

  • Creation phase
  • Execution phase

During the creation phase, you will do the following:

  • Bind this
  • Allocate memory space for functions and variables
  • Initialize related variables to undefined

The variable promotion and function promotion we mentioned everyday are done in the creation stage, so the following writing method will not report an error:

console.log(msg);
add(1,2)

var msg = 'hello'
function add(a,b){
  return a + b;
}

Because the memory space of msg and add has been allocated in the creation phase before execution.

Common error types of JavaScript

In order to understand the module import of ESM more easily, here is another knowledge point - the common error reporting types of JavaScript.

1,RangeError

This kind of error is very 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 very common. Printing a non-existent value is ReferenceError:

hello

// out: 
// ReferenceError: hello is not defined

3,SyntaxError

SyntaxError is also very common. When the syntax does not conform to the JS specification, this error will be reported:

console.log(1));

// out:
// console.log(1));
//               ^
// SyntaxError: Unexpected token ')'

4,TypeError

TypeError is also very common. When a basic type is used as a function, this error will be reported:

var a = 1;
a()

// out:
// TypeError: a is not a function

Among the above Error types, syntax Error is the most special because it is an Error thrown out during the compilation stage. If a syntax Error occurs, no line of JS code will be executed. Other types of exceptions are errors in the execution stage. Even if an Error is reported, the script before the exception will be executed.

What is a compile time output interface? What is runtime loading?

ESM is called compile time output interface because its module parsing occurs in the compilation stage.

In other words, the import and export keywords are module parsed in the compilation stage. If the use of these keywords does not comply with the syntax specification, syntax errors will be thrown in the compilation stage.

For example, according to the ES6 specification, import can only be declared at the top level of the module, so the following writing method will directly report syntax errors, and there will be no log printing, because it has not entered the execution stage at all:

console.log('hello world');

if (true) {
  import { resolve } from 'path';
}

// out:
//   import { resolve } from 'path';
//          ^
// SyntaxError: Unexpected token '{'

For the corresponding CommonJS, its module parsing occurs in the execution stage, because require and module are essentially functions or objects. These functions or objects will be instantiated only when the execution stage is running. Therefore, it is called runtime loading.

It should be emphasized here that unlike CommonJS, the object import ed in ESM is not an object, nor is the object export ed. For example, the following wording will prompt syntax errors:

// syntax error! This is not deconstruction!!!
import { a: myA } from './a.mjs'

// syntax error!
export {
  a: "a"
}

The usage of import and export is much like importing an object or exporting an object, but it has nothing to do with the object. Their usage is designed at the ECMAScript language level, and the use of "coincidental" objects is similar.

Therefore, in the compilation stage, the value introduced in the import module points to the value exported in export. If readers understand linux, it is a bit like a hard link in linux, pointing to the same inode. Or take stack and heap as an analogy, which is like two pointers pointing to the same stack.

Loading details of ESM

Before explaining the loading details of ESM, it is very important to understand that variable promotion and function promotion also exist in ESM.

Take the front demos/02 The circular reference example mentioned in is transformed into the circular reference of ESM version. See demos/04 , the code entry is app js:

import './a.mjs';

Look/ a.mjs Code:

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
}

Look again/ b.mjs Code:

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 references each other at the beginning of the file.

Execute node app MJS view running 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 will find an abnormal error of ReferenceError, suggesting that variables cannot be used before initialization. This is because we use let to define variables and const to define functions, which makes it impossible to promote variables and functions.

How to modify it to work normally? In fact, it's very simple: use var instead of let, and use function to define functions. Let's see demos/05 To see the effect:

Look/ a.mjs Code:

console.log('b val', b);

console.log('setB to bb');

setB('bb')

var a = 'a';

function setA(newA) {
  a = newA;
}

export {
  a,
  setA
}

Look again/ b.mjs Code:

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
}

Execute node app MJS view running 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 without exception and error.

Here we can talk about the loading details of ESM in detail. It is actually the same as the module of CommonJS mentioned earlier_ The load function does something similar:

  1. Check the cache. If the cache exists and has been loaded, extract the corresponding value directly from the cache module without the following processing
  2. If the cache does not exist, create a new Module instance
  3. Put this Module instance into the cache
  4. Load the file through this Module instance
  5. When the file is loaded into the global execution context, there will be a creation phase and an execution phase. In the creation phase, functions and variables are promoted, and then the code is executed.
  6. Returns the exports of this Module instance

combination demos/05 For the cyclic loading of, let's make a detailed explanation:

When When MJS loads a.mjs, the Module will check whether there is a.mjs in the cache and find no, so it creates a new a.mjs Module, puts the Module in the cache, and then loads the a.mjs file itself.

When loading the a.mjs file, memory space will be allocated for the function setA and variable a in the global context during the creation phase, and the initialization variable a is undefined. In the execution stage, it is found that the first line is to load b.mjs. It will check whether there is b.mjs in the cache. If it is found that there is no, new a b.mjs module, put this module into the cache, and then load the b.mjs file itself.

When loading the b.mjs file, memory space will be allocated for the function setB and variable B in the global context during the creation phase, and the initialization variable B is undefined. In the execution stage, it is found that the first line is to load a.mjs. It will check whether there is a.mjs in the cache. It is found that there is, so import returns the corresponding value exported by a.mjs in the cache.

Although a.mjs has not been executed at this time, its creation phase has been completed, that is, the setA function and variable a with undefined value already exist in memory. So at this time, you can print a normally in b.mjs and use setA function without exception throwing error.

Talk about the difference between ESM and CommonJS

Differences: this points differently

this point of CommonJS can be viewed Source code:

var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);

It is clear that this refers to the default exports of the current module;

ESM is designed at the language level to be undefined.

Differences:__ filenameļ¼Œ__ dirname exists in CommonJS but not in ESM

In CommonJS, the execution of the module needs to be wrapped with functions, and some common values can be specified for viewing Source code:

NativeModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

So we can use it directly__ filename,__ dirname. ESM has no such design, so it cannot be used directly in ESM__ Filename and__ dirname.

Similarities: both ESM and CommonJS have caches

This is consistent with the two module schemes. Both modules will be cached. After the module is loaded once, it will be cached, and the modules in the cache will be used for subsequent loading.

Reference documents

Topics: Javascript node.js Front-end ECMAScript commonjs