Source code analysis of node modularization

Keywords: node.js

Introduction

First of all, let's talk about the difference between CommonJS module and ES6 module. Here we will directly give the difference between them.

  • The CommonJS module outputs a copy of the value, and the ES6 module outputs a reference to the value.
  • The CommonJS module is run-time loading, and the ES6 module is a compile time output interface.
  • In the ES6 module, the top-level this points to the undefined; the top-level this of the CommonJS module points to the current module

Source code analysis of commonJS modularization

First of all, nodejs Module packer

(function(exports, require, module, __filename, __dirname) {
// The code for the module is actually here
});

Here are the node s Source code of Module

Let's take a look at what we need a file to do.

Module.prototype.require = function(id) {
  validateString(id, 'id');
  requireDepth++;
  try {
    return Module._load(id, this, /* isMain */ false);
  } finally {
    requireDepth--;
  }
};

At least it proves that CommonJS module is run-time loading. Because require is actually a method of module, a js module can only load another file when it runs to the require code of a module.

Then it points to the "load method". Here is a detail of isMain. In fact, node is used to distinguish whether the load module is the main module. Because it is require d, it must not be a main module, except for the cyclic reference scenario at that time.

Module.prototype._load

Next, find the "load" method.

Module._load = function(request, parent, isMain) {
  let relResolveCacheIdentifier;

  const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  const mod = loadNativeModule(filename, request, experimentalModules);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // Don't call updateChildren(), Module constructor already does.
  const module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename;
  }

  let threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
      if (parent !== undefined) {
        delete relativeResolveCache[relResolveCacheIdentifier];
      }
    }
  }

  return module.exports;
};

First of all, look at the cache. It actually deals with multiple requirements. From the source code, you can find a module that requires multiple times. The node always uses the first module object and does not update it. The details of cache are related to why the variables output by node module cannot be changed at runtime. Because even if you change the output variables of a module during the operation, and then require again in another place, module.exports will not change because of the cache. However, it is not clear that the CommonJS module outputs a copy of the value.

Next, let's look at the module.load running after the instantiation of new Module(filename, parent)
The core code we focus on is

Module._extensions[extension](this, filename);
Here is the loaded code. Then let's take a look at the loading of js file.

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

Module.prototype._compile

Here is the process of loading the js file we wrote.
The following code has been greatly reduced

Module.prototype._compile = function(content, filename) {
  const compiledWrapper = wrapSafe(filename, content, this);
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  var result;
  const exports = this.exports;
  const thisValue = exports;
  const module = this;
result = compiledWrapper.call(thisValue, exports, require, module,
                                  filename, dirname);

  return result;
};

First
const require = makeRequireFunction(this, redirects);
This is the key to how the actual require keyword works in the code. It's not complicated. It's mainly about how to find files for the input parameters. Here, I skip the detailed suggestions and take a look at the official documents of the node. This is the ironclad proof that the 'CommonJS module is loaded at run time'. In fact, require depends on the injection of node module at execution time. Internal require modules need to be compile d at run time.

In addition, notice that this.exports is passed to wrapSafe as a parameter, and the entire execution scope is locked on this.exports. Here is the source of the sentence "the top layer of CommonJS module points to the current module".

Take a look at the function wrapSafe formed by the core module.

function wrapSafe(filename, content, cjsModuleInstance) {
  ...
  let compiled;
  try {
    compiled = compileFunction(
      content,
      filename,
      0,
      0,
      undefined,
      false,
      undefined,
      [],
      [
        'exports',
        'require',
        'module',
        '__filename',
        '__dirname',
      ]
    );
  } catch (err) {
    ...
  }

  return compiled.function;
}

The core code can be said to be very few, that is, the constructor of a closure. This is the module wrapper mentioned at the beginning of the article.
compileFunction. This interface can be viewed from the [api] corresponding to node.( http://nodejs.cn/api/vm.html#...
).

Let's take a look at the whole simplified version of node's official require ment.

function require(/* ... */) {
    //Corresponding to this.export = {} in new Module
  const module = { exports: {} };
  
  //The code here corresponds to module.load()
  ((module, exports) => {
    // Module code here. In this example, define a function.
    function someFunc() {}
    exports = someFunc;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = someFunc;
    // At this point, the module will now export someFunc, instead of the
    // default object.
  })(module, module.exports);
  
  //Pay attention to the last output of "load"
  return module.exports;
}

At this time, let's compare the code of "load" to see if it's suddenly realized.

Finally, the explanation that the 'CommonJS module outputs a copy of the value' has been explained. In the cache mechanism, it has been explained why repeated require ments will never be repeated. In the above function, you can see the copy of the value in the exports we use.
So far, the modularity of the whole node has been clearly explained.

Posted by th3void on Wed, 23 Oct 2019 19:10:58 -0700