The two specifications have been bothering me, because they are all very like keywords: import/export/export default/require/module. Exports… Always stupid not to know the difference.

The difference between them is only heard that a dynamic load, a static load; An export copy and an export reference. What does that mean? Why?

To solve this puzzle, I used WebPack + Babel to package the module code from both specifications into ES5 to see what they do. This article first explains their respective concepts and performance, and then analyzes the code, analysis principles. (The main reason to use Babel is to convert the ES6 Module to ES5 and see how it works internally (update: WebPack natively supports import/export, no need to use Babel translation))

Why modularity

Without modularity, importing a JS file from the bottom of the body must be in the correct reference order, or it will not run. However, when the number of JS files is too large, there is uncertainty in the dependencies between the files, which cannot guarantee the correct order, hence the emergence of modularity.

The WebPack configuration for the packaged code

The minimal configuration is as follows, mainly source-map and development mode configuration, so that the packaged code is plain code, uncompressed, and not included in eval, which makes it easier to read. Then you can create two JS files to try out. (WebPack version 4.43.0)

// webpack.config.js
{
// ...
  mode: 'development'.devtool: 'source-map'.module: {
    rules: [{test: /\.js$/,
        exclude: /(node_modules)/,
        loader: 'babel-loader',}}]// ...
}

// .babelrc
{
  "presets": [
    "@babel/preset-env"]}Copy the code

CommonJS specification

The implementation of the CommonJS specification is very simple, as mentioned above.

concept

The CommonJS specification is the standard for node.js processing modules. The NPM ecosystem is built on top of the CommonJS standard.

Variables, functions, and objects can be exported. It is now the most frequently used. It uses synchronous loading to load modules out at once.

Example:

/ * * / is deduced
// Exports directly
exports.uppercase = str= > str.toUpperCase();
// exports
module.exports.a = 1;
/ / rewrite module exports
module.exports = { xxx: xxx };

/ * import * /
 // Access package.a/package.b...
const package = require('module-name');
// Structure assignment
const { a, b, c } = require('./uppercase.js');
Copy the code
The principle of analysis

Here’s a package of two referenced modules, stripped of all the bells and whistles and stripped of their true colors.

Source:

// index.js
const m = require('./module1.js');
console.log(m)

// module1.js
const m = 1;
module.exports = m;
Copy the code

The package file is as follows:

Can clearly see that the whole package webpack code after processing has become an immediate execution of a function, the parameter is an object containing all of the modules, each file is shown as a key/value pairs, using the file path string as the property name, the code within the file, which is the content of a module are all packing into a function as the value of the attribute. The require used internally in the module is also replaced with the __webpack_require__ method.

// 1. Is a function that executes immediately
(function (modules) {
  var installedModules = {};
  
  // 4. Execute the function
  function __webpack_require__(moduleId) {
    // 5. Check if there is a cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 6. Create a module and store it in the cache
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false.exports: {}};// 7. Fetch and execute from the module object according to the module ID
    // this is bound to module.exports and injected into module, module.exports
    modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
    // 8. Mark the module as loaded
    module.l = true;
    // 9. Return module.exports
    return module.exports;
  }
  // 3. Pass in the entry name
  return __webpack_require__(__webpack_require__.s = "./index.js"); ({})// 2. Pass the module object as a parameter
  "./index.js":
    (function (module.exports, __webpack_require__) {
      var m = __webpack_require__("./module1.js")
      console.log(m)
    }),

  "./module1.js":
    (function (module.exports) {
      var m = 1;
      module.exports = m; })});Copy the code

Follow the execution process to discover the CommonJS module processing flow:

  1. call__webpack_require__To create a module for the module (file) to execute.
var module = {
  i: moduleId,
  exports: {}}Copy the code
  1. Call the wrapped function of this module with the call method, bind this to module.exports, pass in module and module.exports, and__webpack_require__Methods.
modules[moduleId].call(module.exports, module.module.exports, __webpack_require__)
Copy the code
  1. If there are other modules required inside the module, __webpack_require__ is called again, repeating the process recursively.
  2. Returns module.exports after a module completes execution.

}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} You can even write this.exports. XXX = XXX to have the same effect; If other modules are imported, they also receive module.exports objects.

Exports = {XXX: XXX}, so it makes no sense to do anything with exports and this.exports.

ES6 Module

concept

Before ES6, JS had no module system. There are only community-specified module loading schemes such as CommonJS for the server side and AMD for the browser side.

ES6 does not export an object and cannot reference the module itself. The module’s methods are loaded separately. Therefore, it can be loaded at compile time (that is, statically loaded), so static analysis can be performed, and module dependencies and input and output variables can be determined at compile time, improving efficiency.

CommonJS and AMD, on the other hand, output objects that need to be looked up when they are introduced, so module dependencies and input and output variables can only be determined at run time (i.e., runtime loading), so there is no “static optimization” at compile time.

The following is an example:

/ * * / is deduced
// Export a variable
export let firstName = 'Michael';
// Export multiple variables
let firstName = 'Michael';
let lastName = 'Jackson';
export { firstName, lastName };
// Export a function
export function multiply(x, y) { 
  return x * y;
}
// Rename the exported variable
export {
  v1 as streamV1,
  v2 as streamV2
};
// The default output (essentially assigning the output variable to default), which you can import with any name and without opening the parentheses {} :
export default function crc32() {}

/ * * /
// Import can only be placed at the top level of the file
import { stat, exists, readFild } from 'fs';
import { lastName as surname } from './profile.js'
// Introduce the entire module, and then use cirke.xxx to get the internal variables or methods
import * as circle from './circle';
import crc from 'crc32';
Copy the code
The principle of analysis

Source:

// index.js
import { m } from './module1';
console.log(m);

// module1.js
const m = 1;
const n = 2;
export { m, n };
Copy the code

After the package:

// 1. Is an immediate execution function
(function (modules) {
  var installedModules = {};
  // 4. Process the entry file module
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 5. Create a module
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false.exports: {}};// 6. Execute the entry file module function
    modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
    module.l = true;
    / / 7. Return
    return module.exports;
  }
  __webpack_require__.d = function (exports, name, getter) {
    if(! __webpack_require__.o(exports, name)) { // declare if name is an attribute of exports
      Object.defineProperty(exports, name, {enumerable: true.get: getter}); }}; __webpack_require__.r =function (exports) {
    if (typeof Symbol! = ='undefined' && Symbol.toStringTag) {
      // Symbol. ToStringTag is a property of an Object. The value represents the Object's custom type [Object Module].
      / / are usually only as the Object. The prototype. The toString () returns a value
      Object.defineProperty(exports.Symbol.toStringTag, {value: 'Module'});
    }
    Object.defineProperty(exports.'__esModule', {value: true});
  };
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };
  // 3. Pass in the entry file ID
  return __webpack_require__(__webpack_require__.s = "./index.js"); ({})// 2. Pass the module object as a parameter
  "./index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      // __webpack_exports__ is module.exports
      "use strict";
      // Added __esModule and Symbol. ToStringTag attributes
      __webpack_require__.r(__webpack_exports__);
      var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
      console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["m"])}),"./module1.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      // add m/n to module.exports and set the getters to return directly
      __webpack_require__.d(__webpack_exports__, "m".function () {returnm; }); __webpack_require__.d(__webpack_exports__,"n".function () {returnn; });var m = 1;
      var n = 2; })});Copy the code

As you can see, much like CommonJS, you pass the module object to a function that executes immediately. It’s just a little more complicated inside the module functions. The first two steps of executing a module are the same as CommonJS: create a module, bind this to the module, and pass in module and module.exports objects.

Within a module, the process of exporting a module is:

  1. First,__webpack_exports__Module. Exports objects add a Symbol. ToStringTag attribute with a value of{value: 'Module'}, which makes module.exports calls to the toString method return[Object Module]To indicate that this is a module.
  2. Add the variable to module.exports and set the getter for the variable, which simply returns the value of the same name.

Imports also behave differently, rather than just importing the module.exports object, they do some extra work. In this example, the index.js file introduces m, and then prints m. However, the result of the package is that the imported M is not the same as the accessed M. When accessing m, we actually access m[‘m’], which means that webPack automatically helps us access the internal properties of the same name.

import { m } from './module1';
console.log(m);

// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["m"])
Copy the code

Different import and export methods will also have different performance:

  1. Different ways to export performance:
  • Method one: directly derived, can only be these three forms, these three are equivalent. Note that export cannot be followed by a constant, because export outputs an interface that corresponds to a variable. For example:var a = 1; export a;The equivalent ofexport 1It doesn’t make sense. It’s an error.
export { bar, foo }
export var bar = xxx
export function foo = xxx
Copy the code
const obj = { a: 1 };
export { obj };

// webpack 
// __webpack_exports__ is the result of export
__webpack_require__.d(__webpack_exports__, "obj".function() { return obj; });
var obj = { a: 1 };

// The __webpack_require__.d function first determines whether the variable to be exported is a property of __webpack_exports__
// If not, hang the variable on __webpack_exports__ and set the getter
__webpack_require__.d = function(exports, name, getter) {
  if(! __webpack_require__.o(exports, name)) {
    Object.defineProperty(exports, name, { enumerable: true.get: getter }); }};Copy the code
  • Method 2: Export default: simply put the variable to be exported into the object and then hang it in the__webpack_exports__.defaulton
let obj = { a: 1 }
export default { obj }

// webpack
var obj = { a: 1 };
__webpack_exports__["default"] = ({ obj: obj });

// There are no restrictions on the value following export default
export default obj
// webpack
__webpack_exports__["default"] = (obj);

export default obj.a
// webpack
__webpack_exports__["default"] = (obj.a);

export default 1
// webpack
__webpack_exports__["default"] = (1);
Copy the code
  1. Different ways to import performance:
  • Method 1: Import the entire module: Directly obtain the value of the entire module__webpack_exports__If export default is not used, an undefined value will be obtained. If export default is not used, an undefined value will be obtained.
import obj from './module1'
console.log(obj) // No default will get undefined
obj.c = 2; // If there is no default, an error will be reported

// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["default"]);
_module1__WEBPACK_IMPORTED_MODULE_0__["default"].c = 2;
Copy the code
  • Method two: with*Overall import, will not automatically find the inner attributes, direct access__webpack_exports__
import * as obj from './module1'
console.log(obj)
obj.c = 2;

// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__);
_module1__WEBPACK_IMPORTED_MODULE_0__["c"] = 2;
Copy the code
  • Method 3: import specific modules by deconstructing assignment. When accessing the module, the attribute value with the same name in curly braces will be automatically searched.
import { obj } from './module1'
console.log(obj)
obj.c = 2;

// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["obj"]);
_module1__WEBPACK_IMPORTED_MODULE_0__["obj"].c = 2;
Copy the code

CommonJS and ES6 Module comparison

Now you know what dynamic load, static load, copy, reference all mean at the beginning of this article.

CommonJS exports objects, and the internal variables are assigned to the properties of the object at the moment they are exported. This means that “CommonJS outputs a copy of the value”. If you change a variable in a module, other modules will not notice it because it is no longer relevant. But objects are still affected, because objects only copy references to objects.

Also, because CommonJS exports objects, the contents of the objects are not read during compilation, and it is not clear what variables are exported inside the objects or whether they were imported from other files. Only when the code is running can you access the properties of the object and determine the dependencies. This is why CommonJS modules are loaded dynamically.

For THE ES6 Module, the getter is defined internally for each variable, so when other modules access the variable after import, the getter is triggered, and the variable of the same name is returned in the Module. If the value of the variable changes, the external reference will also change.

Export Default, however, is not a getter. It is also a direct assignment, so the output is also a copy. For example, in the following code, you can see that you simply attach a copy of the value of the variable m to the default property. (As noted in the comment section, WebPack5 corrects this behavior and default also getters.)

const m = 1;
export default m;

// webpack
(function (module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_require__.r(__webpack_exports__);
  var m = 1;
  __webpack_exports__["default"] = (m);
})
Copy the code

An ES6 Module does not export an object, but an interface, so the dependencies between modules can be determined at compile time. That’s why an ES6 Module is statically loaded. Tree Shaking is based on this feature to shake down unwanted modules at compile time.

ES6 Module also provides an import() method to dynamically load the Module, returning a Promise.

References

  1. Es6.ruanyifeng.com/#docs/modul…