Module 与 require 源码解析

Cycles

a.js:

console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

b.js:

console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

main.js:

console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
$ node main.js
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true

Cache

a.js:

module.exports = new Date();

main.js:

console.log(require('./a.js'));
console.log(require('./a.js'));
setTimeout(()=>console.log(require('./a.js')),5000)
三次输出的值每次都是一样的

require 源码解析

require 源码的位置在 lib/internal/modules/cjs/loader.js

Module 构造函数

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

Module 对象的属性是什么 a.js:

module.exports = exports = {
    a: 123
};
console.log('module.id: ', module.id);
console.log('module.exports: ', module.exports);
console.log('module.parent: ', module.parent);
console.log('module.filename: ', module.filename);
console.log('module.loaded: ', module.loaded);
console.log('module.children: ', module.children);
console.log('module.paths: ', module.paths);
$ node a.js
module.id:  .
module.exports:  { a: 123 }
module.parent:  null
module.filename:  /Users/catchme/Sites/rpc/module/a.js
module.loaded:  false
module.children:  []
module.paths:  [ '/Users/catchme/Sites/rpc/module/node_modules',
  '/Users/catchme/Sites/rpc/node_modules',
  '/Users/catchme/Sites/node_modules',
  '/Users/catchme/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

如果没有父模块,直接调用当前模块,parent 属性就是 null,id 属性就是一个点。filename 属性是模块的绝对路径,path 属性是一个数组,包含了模块可能的位置。另外,输出这些内容时,模块还没有全部加载,所以 loaded 属性为 false 。

b.js:

var a = require('./a.js');
$ node b.js
module.id:  /Users/catchme/Sites/rpc/module/a.js
module.exports:  { a: 123 }
module.parent:  Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Users/catchme/Sites/rpc/module/b.js',
  loaded: false,
  children: 
   [ Module {
       id: '/Users/catchme/Sites/rpc/module/a.js',
       exports: [Object],
       parent: [Circular],
       filename: '/Users/catchme/Sites/rpc/module/a.js',
       loaded: false,
       children: [],
       paths: [Array] } ],
  paths: 
   [ '/Users/catchme/Sites/rpc/module/node_modules',
     '/Users/catchme/Sites/rpc/node_modules',
     '/Users/catchme/Sites/node_modules',
     '/Users/catchme/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }
module.filename:  /Users/catchme/Sites/rpc/module/a.js
module.loaded:  false
module.children:  []
module.paths:  [ '/Users/catchme/Sites/rpc/module/node_modules',
  '/Users/catchme/Sites/rpc/node_modules',
  '/Users/catchme/Sites/node_modules',
  '/Users/catchme/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

由于 a.js 被 b.js 调用,所以 parent 属性指向 b.js 模块,id 属性和 filename 属性一致,都是模块的绝对路径。

require方法

Module.prototype.require = function(id) {
  validateString(id, 'id'); //判断typeof id === 'string'
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id,
                                    'must be a non-empty string');
  }
  return Module._load(id, this, /* isMain */ false); // 因为是引入模块,require.main === module 为 false
};

Module._load 方法

Module._load = function(request, parent, isMain) {
  // 这一段就在打印输出  
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }
  // 计算文件的绝对路径
  // 如果是核心模块直接返回核心模块的名字
  var filename = Module._resolveFilename(request, parent, isMain);

  // 在缓存中取该模块
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    // 如果取到了就给调用require的模块添加添加(push)children[]
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }
  // 如果是核心模块就用NativeModule.require方法来导入
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

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

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

  Module._cache[filename] = module;
  // 这个方法会调用 Module.prototype.load
    // 通过这个方法确定文件的后缀(.js .json)
    // var extension = findLongestRegisteredExtension(filename);
    // 每一种不同文件的后缀就会使用不同的载入方法
    // Module._extensions[extension](this, filename);
  tryModuleLoad(module, filename);

  return module.exports;
};

Module.__extensions载入方式

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};


// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  return process.dlopen(module, path.toNamespacedPath(filename));
};

if (experimentalModules) {
  if (asyncESM === undefined) lazyLoadESM();
  Module._extensions['.mjs'] = function(module, filename) {
    throw new ERR_REQUIRE_ESM(filename);
  };
}

require() 函数同步还是异步?

正如Module._extensions['.js']var content = fs.readFileSync(filename, 'utf8');所以 require 函数是同步的。
那么为什么设计成同步的呢?
因为 CommonJS 定义了模块同步加载同步执行,所以Node用同步的实现。

Node的module.js里用了readFileSync而不用readFile?

  • CommonJS 模块是同步加载和同步执行,AMD 模块是异步加载和异步执行,CMD(Sea.js)模块是异步加载和同步执行。ES6 的模块体系最后选择的是异步加载和同步执行。也就是 Sea.js 的行为是最接近 ES6 模块的。不过 Sea.js 这样做是需要付出代价的——需要扫描代码提取依赖,所以它不像 CommonJS/AMD 是纯运行时的模块系统。--贺师俊
  • 更加的简单
  • 开销更小
  • 历史原因 Node(2009),只有 AMD 和 CommonJS,而与 ES6 更加接近 CMD(Sea.js 2010)
  • Node 的模块设计具有缓存,如果异步 require 的时候执行了一个同步的 require 就会出现不可预知的问题

Module.prototype._compile 方法

Module.prototype._compile = function(content, filename) {
  // 移除
  content = stripShebang(content);
  // 将代码 包裹
  //(function(exports, require, module, __filename, __dirname) {
         // 模块的代码实际上在这里
  // });
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true,
    importModuleDynamically: experimentalModules ? async (specifier) => {
      if (asyncESM === undefined) lazyLoadESM();
      const loader = await asyncESM.loaderPromise;
      return loader.import(specifier, normalizeReferrerURL(filename));
    } : undefined,
  });

  var inspectorWrapper = null;
  if (process._breakFirstLine && process._eval == null) {
    if (!resolvedArgv) {
      // we enter the repl if we're not given a filename argument.
      if (process.argv[1]) {
        resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
      } else {
        resolvedArgv = 'repl';
      }
    }

    // Set breakpoint on module start
    if (filename === resolvedArgv) {
      delete process._breakFirstLine;
      inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
    }
  }
  var dirname = path.dirname(filename);
  var require = makeRequireFunction(this);
  var depth = requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result;
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
                              require, this, filename, dirname);
  } else {
    // 入exports、require、module三个全局变量
    result = compiledWrapper.call(this.exports, this.exports, require, this,filename, dirname);
  }
  if (depth === 0) stat.cache = null;
  return result;
};

vm.runInThisContext方法

这个方法类似与 eval

results matching ""

    No results matching ""