当前位置:网站首页>Explain ESM module and commonjs module in simple terms

Explain ESM module and commonjs module in simple terms

2022-07-06 21:37:00 outsider

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 modulerequire 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 , after require 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 module exports
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 top wrapper And we used that myRequire and myModule To distinguish native require and module, 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 :

  1. Check cache , If the cache exists and is already loaded , Directly back to cache , Do not do the following processing
  2. If the cache does not exist , Create a new one Module example
  3. Put this Module The instance is put into the cache
  4. Through this Module Instance to load the file
  5. 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 :

  1. 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
  2. If the cache does not exist , Create a new one Module example
  3. Put this Module The instance is put into the cache
  4. Through this Module Instance to load the file
  5. 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 .
  6. 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

原网站

版权声明
本文为[outsider]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/02/202202131118365609.html