经由过程源码剖析 Node.js 中一个文件被 require 后所发作的故事

在 Node.js 中,要说如果有险些会在每个文件都要用到的一个全局函数和一个全局对象,那应当黑白 requiremodule.exports 莫属了。它们是 Node.js 模块机制的基石。人人在运用它们享用模块化的优点时,偶然也不禁猎奇:

  • 为什么它俩运用起来像是全局函数/对象,却在 global 对象下接见不到它们?

'use strict'
console.log(require) // Function 
console.log(module) // Object 
console.log(global.require) // undefined
console.log(global.module) // undefined
  • 这两个“类全局”对象是在什么时刻,怎样天生的?

  • require 一个目次时,Node.js 是怎样替我们找到详细该实行的文件的?

  • 模块内的代码详细是以何种体式格局被实行的?

  • 轮回依靠了怎样办?

让我们从 Node.js 项目的 lib/module.js 中的代码里,细细看一番,一个文件被 require 后,详细发作的故事,从而来解答上面这些题目。

一个文件被 require 后所发作的故事

当我们在命令行中敲下:

node ./index.js

以后,src/node.cc 中的 node::LoadEnvironment 函数会被挪用,在该函数内则会接着挪用 src/node.js 中的代码,并实行 startup 函数:

// src/node.js
// ...

function startup() {
  // ...
  Module.runMain();
}

// lib/module.js
// ...

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

所以,末了会实行到 Module._load(process.argv[1], null, true); 这条语句来加载模块,不过实在,这个Module._loadrequire函数的代码中也会被挪用:

// lib/module.js
// ... 

Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, false);
};

所以说,当我们在命令行中敲下 node ./index.js,某种意义上,能够说随后 Node.js 的表现即为马上举行一次 require , 即:

require('./index.js')

随后的步骤就是 require 一个一般模块了,让我们继承往下看,Module._load 要领做的第一件事,就是挪用内部要领 Module._resolveFilename ,而该内部要领在举行了一些参数预处理后,终究会挪用 Module._findPath 要领,来获得需被导入模块的完全途径,让我们从代码中来总结出它的途径分析划定规矩:

// lib/module.js
// ...

Module._findPath = function(request, paths) {
  // 优先取缓存
  var cacheKey = JSON.stringify({request: request, paths: paths});
  if (Module._pathCache[cacheKey]) {
    return Module._pathCache[cacheKey];
  }

  // ...
  for (var i = 0, PL = paths.length; i < PL; i++) {
    if (!trailingSlash) { 
      const rc = stat(basePath);
      if (rc === 0) {  // 如果文件.
        filename = toRealPath(basePath);
      } else if (rc === 1) {  // 如果目次
        filename = tryPackage(basePath, exts);
      }

      if (!filename) {
        // 带上 .js .json .node 后缀举行尝试
        filename = tryExtensions(basePath, exts);
      }
    }

    if (!filename) {
      filename = tryPackage(basePath, exts);
    }

    if (!filename) {
      // 尝试 index.js index.json index.node
      filename = tryExtensions(path.resolve(basePath, 'index'), exts);
    }

    if (filename) {
      // ...
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
  }
  return false;
};

function tryPackage(requestPath, exts) {
  var pkg = readPackage(requestPath); // 猎取 package.json 中 main 属性的值

  // ...
  return tryFile(filename) || tryExtensions(filename, exts) ||
         tryExtensions(path.resolve(filename, 'index'), exts);
}

代码中的前提推断非常清楚,让我们来总结一下:

  • 若模块的途径不以 / 末端,则先搜检该途径是不是实在存在:

    • 若存在且为一个文件,则直接返回文件途径作为效果。

    • 若存在且为一个目次,则尝试读取该目次下的 package.jsonmain 属性所指向的文件途径。

      • 推断该文件途径是不是存在,若存在,则直接作为效果返回。

      • 尝试在该途径后顺次加上 .js.json.node 后缀,推断是不是存在,若存在则返回加上后缀后的途径。

      • 尝试在该途径后顺次加上 index.jsindex.jsonindex.node,推断是不是存在,若存在则返回拼接后的途径。

    • 若仍未返回,则为指定的模块途径顺次加上 .js.json.node 后缀,推断是不是存在,若存在则返回加上后缀后的途径。

  • 若模块以 / 末端,则尝试读取该目次下的 package.jsonmain 属性所指向的文件途径。

    • 推断该文件途径是不是存在,若存在,则直接作为效果返回。

    • 尝试在该途径后顺次加上 .js.json.node 后缀,推断是不是存在,若存在则返回加上后缀后的途径。

    • 尝试在该途径后顺次加上 index.jsindex.jsonindex.node,推断是不是存在,若存在则返回拼接后的途径。

  • 若仍未返回,则为指定的模块途径顺次加上 index.jsindex.jsonindex.node,推断是不是存在,若存在则返回拼接后的途径。

在获得了模块的完全途径后,便该是实行模块了,我们以实行 .js 后缀的 JavaScript 模块为例。起首 Node.js 会经由历程 fs.readFileSync 要领,以 UTF-8 的花样,将 JavaScript 代码以字符串的情势读出,通报给内部要领 module._compile,在这个内部要领里,则会挪用 NativeModule.wrap 要领,将我们的模块代码包裹在一个函数中:

// src/node.js
// ...

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

所以,这便解答了我们之前提出的,在 global 对象下取不到它们的题目,因为它们是以包裹在外的函数的参数的情势通报进来的。所以趁便提一句,我们平常在文件的顶上写的 use strict ,实在终究声明的并非 script-level 的严厉形式,而都是 function-level 的严厉形式。

末了一步, Node.js 会运用 vm.runInThisContext 实行这个拼接终了的字符串,获得一个 JavaScript 函数,末了带着对应的对象参数实行它们,并将赋值在 module.exports 上的对象返回:

// lib/module.js
// ...

Module.prototype._compile = function(content, filename) {
  // ...

  var compiledWrapper = runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  // ...
  const args = [this.exports, require, this, filename, dirname];
  
  const result = compiledWrapper.apply(this.exports, args);
  // ...
};

至此,一个同步的 require 操纵便圆满结束啦。

轮回依靠

经由历程上文我们已能够晓得,在 Module._load 内部要领里 Node.js 在加载模块之前,起首就会把传模块内的 module 对象的援用给缓存起来(此时它的 exports 属性照样一个空对象),然后实行模块内代码,在这个历程当中逐渐为 module.exports 对象附上该有的属性。所以当 Node.js 这么做时,涌现轮回依靠的时刻,仅仅只会让轮回依靠点取到中心值,而不会让 require 死轮回卡住。一个典范的例子:

// a.js
'use strict'
console.log('a starting')
exports.done = false
var b = require('./b')
console.log(`in a, b.done=${b.done}`)
exports.done = true
console.log('a done')
// b.js
'use strict'
console.log('b start')
exports.done = false
let a = require('./a')
console.log(`in b, a.done=${a.done}`)
exports.done = true
console.log('b done')
// main.js
'use strict'
console.log('main start')
let a = require('./a')
let b = require('./b')
console.log(`in main, a.done=${a.done}, b.done=${b.done}`)

实行 node main.js ,打印:

main start
a starting
b start
in b, a.done=false => 轮回依靠点取到了中心值
b done
in a, b.done=true
a done
in main, a.done=true, b.done=true 

末了

因为 Node.js 中的模块导入和 ES6 范例中的差别,它的导入历程是同步的。所以完成起来会轻易很多,代码量一样也不多。非常引荐人人浏览一下完全的完成。

参考:

    原文作者:菜菜蔡伟
    原文地址: https://segmentfault.com/a/1190000004695582
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞