node模块加载机制

100 阅读2分钟

当我们调用require(...), 它到底是怎么样的一个过程?

require(...) 加载模块

const mod = require('./a.js');

通过跟踪代码,探究一下背后的过程。 注意,下面的代码经过严重简化。

1. require函数

require = function require(path) {
  return mod.require(path);
};

2. mod.require函数

Module.prototype.require = function(id) {
  return Module._load(id, this, /* isMain */ false);
};

3. Module._load函数

Module._load = function(request, parent, isMain) {
  // 获取文件路径
  const filename = Module._resolveFilename(request, parent, isMain);


  // 原生模块对象
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  // 第三方模块创建模块对象,包括node_modules下和src下的
  var module = new Module(filename, parent);
  
  // 加载模块
  module.load(filename);
  
  return module.exports;
}

Module._resolveFilename 函数的作用是找到文件路径。

Module._resolveFilename 函数依赖父模块 parent, 比如父模块的路径为 /project/src/index.js, 那么 require('./a.js') 是相对于父模块的路径来查找的:

require('./a') --> /project/src/a.js require('express') --> /project/node_modules/express/index.js ...

require('express') 为例,将会以如下顺序查找文件,如果文件存在则返回文件决定路径:

  • node_modules/express.js
  • node_modules/express.json
  • node_modules/express.node
  • node_modules/express/package.json 返回main字段指定文件
  • node_modules/express/index.js

4. mod.load函数

Module.prototype.load = function(filename) {
  const extension = findLongestRegisteredExtension(filename);
  Module._extensions[extension](this, filename);
}

根据文件后缀执行不同的操作,比如 .js、.json、.node(),json 文件会直接读取文件内容,JSON.parse 直接输出, node 文件会使用 process.dlopen() 执行文件。

5. 对js文件处理的部分

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

对于 js 文件,先读取 js 代码内容,然后编译代码。

6. mod._compile函数

Module.prototype._compile = function(content, filename) {
  var wrapper = Module.wrap(content);
  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });
  compiledWrapper.call(
    this.exports,
    this.exports, // 模块代码运行会在this.exports上添加变量,这个也是require(...)返回的结果
    require,
    this,
    filename,
    dirname
  );                               
}

编译代码主要用到 vm 模块,将代码编译成字节码,让v8虚拟机运行。

Module.wrap
const Module = require('module');
Module.wrap('console.log(1);');

// (function (exports, require, module, __filename, __dirname) { console.log(1);
// });

Module.wrap的作用就是给代码外包裹一个函数。

所以我们在写 nodejs 代码时可以直接使用 exports、require、module、__filename、__dirname 而不需要 require, 因为它们已经被传入。

vm.runInThisContext

vm.runInThisContext(...) 相当于 script = vm.Script(...); script.runInThisContext(...) 的组合。

main模块的加载

当我们运行node index.js, 会调用runMain函数:

// lib/internal/modules/run_main.js

function executeUserEntryPoint(main = process.argv[1]) {
  Module._load(main, null, true);
}

在内部直接调用 Module._load,因为它是第一个被调用的模块,所以参数 parent 为 null, isMain 为 true。

参考链接: