preface

In ES6, we know that import and export replace require and module.exports to import and export modules, but if you don’t know the ES6 module features, the code can run some strange results. In this article, I will reveal the ES6 module mechanics for you.

ES6 module features

I won’t cover the basic ES6 Module usage, but if you haven’t already used ES6 modules, I recommend: ECMAScript 6 Starter – Module syntax

Speaking of ES6 module features, let’s talk about the differences between ES6 modules and CommonJS modules.

ES6 module differs from CommonJS module in the following two aspects:

  1. The ES6 module outputs references to values, and the output interface is dynamically bound, whereas CommonJS outputs copies of values
  2. ES6 modules are executed at compile time, whereas CommonJS modules are always loaded at run time

How do I understand this? Let’s look at it step by step:

CommonJS copy of the output value

First, in CommonJS modules, if you require a module, you execute the file’s code and end up getting a copy of the module’s module.exports object.

// a.js var b = require('./b'); console.log(b.foo); setTimeout(() => { console.log(b.foo); console.log(require('./b').foo); }, 1000); // b.js let foo = 1; setTimeout(() => { foo = 2; }, 500); module.exports = { foo: foo, }; // Run: node a.js // Result: // 1 // 1 // 1Copy the code

As you can see from the code above, you’re just getting a copy of the module’s output object. Foo in B is irrelevant to Foo in A, so if you want to get values in a module dynamically in CommonJS, you need to make use of the function delay feature.

// a.js var b = require('./b'); console.log(b.foo()); setTimeout(() => { console.log(b.foo()); console.log(require('./b').foo()); }, 1000); // b.js let foo = 1; setTimeout(() => { foo = 2; }, 500); module.exports = { foo: () => { return foo; }}; // Run: node a.js // Result: // 1 // 2/2Copy the code

So we can sum it up:

  1. The position of require in the CommonJS module will affect the output and produce a copy of the value
  2. The CommonJS module repeatedly introduces modules that are not executed repeatedly, and retrieving modules again will only get a copy of the previously acquired modules

References to ES6 output values

In the ES6 module, however, instead of generating a copy of the output object, values in the module are dynamically associated.

// a.js import { foo } from './b'; console.log(foo); setTimeout(() => { console.log(foo); import('./b').then(({ foo }) => { console.log(foo); }); }, 1000); // b.js export let foo = 1; setTimeout(() => { foo = 2; }, 500); // execute: babel-node a.js // Execute: // 1 // 2 // 2Copy the code

ES6 statically compiled, CommonJS runtime loaded

On the second point, ES6 modules execute at compile time resulting in the following two characteristics:

  1. Import commands are statically parsed by the JavaScript engine, taking precedence over the rest of the module.
  2. The export command has the effect of pre-declaring variables.

Import is preferentially executed:

From the first point of view, introducing an import module anywhere in the file will be brought forward to the top of the file.

// a.js console.log('a.js') import { foo } from './b'; // b.js export let foo = 1; Console. log('b.js executes first '); // Result: // b.js Is executed first // a.jsCopy the code

The import is introduced later than console.log(‘a’), but it is statically analyzed by the JS engine and is placed at the top of the module’s execution, which is better than the rest of the module’s execution.

Since import is executed statically, it has a lifting effect, that is, the position of the import command in the module does not affect the output of the program.

Export variable declaration enhancement:

Normal import modules are not able to see the feature of variable declaration enhancement, they need to be seen through cyclic dependency loading.

// a.js
import { foo } from './b';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
  console.log('bar2');
}
export function bar3() {
  console.log('bar3');
}

// b.js
export let foo = 1;
import * as a from './a';
console.log(a);

// 执行结果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js
Copy the code

It can be seen intuitively from the above example that module A references module B, and module B also references module A. The variables declared by export are also better than the execution of other contents of the module, but the specific variable assignment needs to wait until the corresponding code is executed. (Of course, function declarations and expression declarations are not the same, this point is the same as JS function properties, here will not be explained more)

Now that we’ve covered the differences between ES6 modules and CommonJS modules, let’s talk about the similarities:

Modules are not executed repeatedly

This makes sense, whether it’s an ES6 module or a CommonJS module, when you introduce the same module repeatedly, the module will only execute once.

// a.js import './b'; import './b'; // b.js console.log(' only executed once '); // Execute result: // Execute only onceCopy the code

In combination with the above features, let’s take a look at a classic example, loop dependencies. Once you understand the above features, it will be easy to understand the next time you encounter the results of module loop dependencies.

CommonJS module cyclic dependencies

Let’s start with the following example:

// a.js console.log('a starting'); exports.done = false; const b = require('./b'); console.log('in a, b.done =', b.done); exports.done = true; console.log('a done'); // b.js console.log('b starting'); exports.done = false; const a = require('./a'); console.log('in b, a.done =', a.done); exports.done = true; console.log('b done'); // node a.js // Result: // a starting // b starting // in B, a.done = false // b done // in A, b.Done = true // a doneCopy the code

Node exports a module from B. Node exports a module from B. Node exports a module from B. Node exports a module from B. Node exports a module from B. Node exports a module from B. Node exports a module from B. Done in module.exports is still false because module A imported module B before reassigning done. When the done value of module B is output in module A, module B has finished, so the done value of module B is true.

In the CommonJS specification, when a require() statement is encountered, the code in the Require module will be executed and the result will be cached. The next time it is loaded, it will not be executed again, but will fetch the cached result directly. Because of this, there is no case of infinite loop calls when loop dependencies occur. While this module-loading mechanism can avoid the problem of cyclic dependency reporting errors, it is possible to make code that does not perform as expected if you are not careful. So you still need to plan carefully when you write code to make sure that the loop module’s dependencies work correctly.

So what can you do to avoid getting tangled up in circular dependencies? One solution would be to export each module first to the exports syntax and then to the Requre statement, using CommonJS caching to export its exports before requiring () other modules to ensure that they get the correct values when used. Such as:

// a.js
exports.done = true;
let b = require('./b');
console.log(b.done)

// b.js
exports.done = true;
let a = require('./a');
console.log(a.done)
Copy the code

This writing method is simple, the disadvantage is to change the writing method of each module, and most students are used to write require statement at the beginning of the file.

ES6 modules have cyclic dependencies

As with CommonJS modules, ES6 no longer executes re-loaded modules, and thanks to the dynamic output binding feature of ES6, ES6 ensures that the current values of other modules are available at any time.

// a.js console.log('a starting') import {foo} from './b'; console.log('in b, foo:', foo); export const bar = 2; console.log('a done'); // b.js console.log('b starting'); import {bar} from './a'; export const foo = 'foo'; console.log('in a, bar:', bar); setTimeout(() => { console.log('in a, setTimeout bar:', bar); }) console.log('b done'); // babel-node a.js // Execution result: // b starting // in a, bar: undefined // b done // a starting // in b, foo: foo // a done // in a, setTimeout bar: 2Copy the code

If you do not understand the results of the implementation, it indicates that you did not understand the previous said ES6 module features, please read again!

Dynamic import ()

ES6 modules are statically analyzed at compile time, taking precedence over the rest of the module, so we can’t write code like this:

if(some condition) {
  import a from './a';
}else {
  import b from './b';
}

// or 
import a from (str + 'b');
Copy the code

Because of compile-time static analysis, we could not concatenate string modules in conditional statements or because these are results that need to be determined at runtime in ES6 modules, so dynamic import() was introduced.

Import () allows you to dynamically import ES6 modules at runtime. When you think of this, you might also think of the require.ensure syntax, but their purposes are quite different.

  • Ensure was created as a webpack product because browsers needed an asynchronous mechanism to load modules asynchronously, thus reducing the size of the initial load file. Therefore, require. Ensure was useless on the server side. Because the server does not load modules asynchronously, modules can be loaded synchronously to meet the application scenario. The CommonJS module can verify that the module is loaded at run time.
  • Import (), on the other hand, is mainly used to solve the problem that ES6 modules cannot determine module references at runtime, so import() is introduced.

Let’s see how it’s used:

  1. Dynamic import() provides a Promise-based API
  2. Dynamic import() can be used anywhere in the script
  3. Import () accepts string literals, and you can construct specifiers according to your needs

Here’s a simple usage example:

// a.js const str = './b'; const flag = true; if(flag) { import('./b').then(({foo}) => { console.log(foo); }) } import(str).then(({foo}) => { console.log(foo); }) // b.js export const foo = 'foo'; // babel-node a.js // execution result // foo // fooCopy the code

Of course, if the import() function on the browser side becomes more versatile, such as asynchronously loading modules on demand, it would be similar to require.ensure.

Since it’s Promise based, if you want to load multiple modules at the same time, you can use promise.all for parallel asynchronous loading.

Promise.all([
  import('./a.js'),
  import('./b.js'),
  import('./c.js'),
]).then(([a, {default: b}, {c}]) => {
    console.log('a.js is loaded dynamically');
    console.log('b.js is loaded dynamically');
    console.log('c.js is loaded dynamically');
});
Copy the code

There’s also the promise.race method, which checks which Promise is resolved or reject first. We can use import() to check which CDN is faster:

Const CDNs = [{name: 'jQuery.com', url: "https://code.jquery.com/jquery-3.1.1.min.js"}, {name: 'googleapis.com', url: "https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"}]; console.log(`------`); console.log(`jQuery is: ${window.jQuery}`); Promise.race([ import(CDNs[0].url).then(()=>console.log(CDNs[0].name, 'loaded')), import(CDNs[1].url).then(()=>console.log(CDNs[1].name, 'loaded')) ]).then(()=> { console.log(`jQuery version: ${window.jQuery.fn.jquery}`); });Copy the code

Of course, if you think this is not elegant enough, you can also use it with async/await syntactic sugar.

async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
Copy the code

Dynamic import() gives us the additional capability of using the ES module in an asynchronous manner. Loading them dynamically or conditionally according to our needs allows us to create more advantageous applications faster and better.

At the end

ES6 module loading mechanism, some strange output will also have their own decision, I hope this article will help you!