Almost all Node.js developers can tell you what the `require()` function does, but how many of us really know how it works? We use it every day to load libraries and modules, but its behavior is a mystery to us.
Out of curiosity, I delved into the core code of node to find out what happened under the engine. But this is not a single function. I found module.js in the node module system. The file contains a surprisingly powerful and relatively unfamiliar core module that controls the loading, compilation and caching of each file. `require()`, its emergence is just the tip of the iceberg.
module.js
The code copy is as follows:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
In module.js, it mainly plays two roles inside Node.js. First, it provides a foundation for all Node.js modules. Each file is a new instance of the base module new, and it still exists even after the file has been run. This is why we are able to attach the properties to module.exports and return them when needed.
The second major task of this module is to handle the module loading mechanism of node. The independent operation of the "require" function we use is actually an abstract concept module.require, which itself is just a simple encapsulation about the Module._load function. This load method handles the actual loading of each file and starts our journey there.
Module._load
The code copy is as follows:
Module._load = function(request, parent, isMain) {
// 1. Check Module._cache for the cached module.
// 2. Create a new Module instance if cache is empty.
// 3. Save it to the cache.
// 4. Call module.load() with your the given filename.
// This will call module.compile() after reading the file contents.
// 5. If there was an error loading/parsing the file,
// delete the bad module from the cache
// 6. return module.exports
};
Module._load is responsible for loading new modules and managing module caches. Cache loaded each module reduces the number of reads of redundant files and can significantly speed up your application. In addition, the shared module instance allows modules with singleton characteristics to remain in the state in the project.
If a module does not exist in the cache, Module._load will create a new base module for the file. It will then tell the module to read the contents of the new file before sending them to module._compile. [1]
If you notice step #6 above, you will see that module.exports has been returned to the user. This is why when you are defining the public interface usage, you use exports and module.exports, because Module._load will next return the contents of require. I was surprised that there were no more features here, but it would be better if there were.
module._compile
The code copy is as follows:
Module.prototype._compile = function(content, filename) {
// 1. Create the standalone requires function that calls module.require.
// 2. Attach other helper methods to require.
// 3. Wraps the JS code in a function that provides our require,
// module, etc. variables locally to the module scope.
// 4. Run that function
};
・This is where the real miracle happens. First, a special, independent operational require function is created for the module. This is a feature we need and are all familiar with. The function itself is just an encapsulation in Module.require, which also contains some less-known auxiliary methods that are easy for us to use:
・Require(): Load an external module
・Require.resolve(): parse a module name to its absolute path
・Require.main: main module
・Require.cache: All cached modules
・ ・Require.extensions: A compilation method that can be used for each valid file type according to its extension
Once the require is ready, the entire loaded source code will be encapsulated in a new function, allowing it to accept require, module, exports and all other exposed variables as parameters. This is a function created just to encapsulate modules to prevent conflicts with Node.js environment.
The code copy is as follows:
(function (exports, require, module, __filename, __dirname) {
// YOUR CODE INJECTED HERE!
});
The Module._compile method is executed synchronously, so the call to Module._load can only wait until the code is run ends and returns module.exprts to the user.
in conclusion
So we have already understood the full code of require and have a preliminary understanding of how it works.
If you've done it all the way, then you're ready for the last secret: require('module'). This is correct, the module system itself can be loaded through the module system. Inception. This may sound strange, but it allows user space to interact with module loading systems without delving into the Node.js core. Popular modules are all built like this. [2]
If you want to know more, please check the module.js source code yourself. There are still many things that will be enough for you to have a headache for a while. The first one can tell me what NODE_MODULE_CONTEXTS is" and why it's added can get bonus points :)
[1] The module._compile method is only used to run JavaScript files. JSON file needs to be parsed through JSON.parse() and returned
[2] However, both modules are built on private module methods such as Module._resolveLookupPaths and Module._findPath. You can think this is not much better...