Node.js Require原始碼粗讀

菠蘿小蘿蔔發表於2019-03-03

作者:肖磊

個人主頁:github

最近一直在用node.js寫一些相關的工具,對於node.js的模組如何去載入,以及所遵循的模組載入規範的具體細節又是如何並不是瞭解。這篇檔案也是看過node.js原始碼及部分文章總結而來:

es2015標準以前,js並沒有成熟的模組系統的規範。Node.js為了彌補這樣一個缺陷,採用了CommonJS規範中所定義的模組規範,它包括:

1.require

require是一個函式,它接收一個模組的識別符號,用以引用其他模組暴露出來的API

2.module context

module context規定了一個模組當中,存在一個require變數,它遵從上面對於這個require函式的定義,一個exports物件,模組如果需要向外暴露API,即在一個exports的物件上新增屬性。以及一個module object

3.module Identifiers

module Identifiers定義了require函式所接受的引數規則,比如說必須是小駝峰命名的字串,可以沒有檔案字尾名,.或者..表明檔案路徑是相對路徑等等。

具體關於commonJS中定義的module規範,可以參見wiki文件

在我們的node.js程式當中,我們使用require這個看起來是全域性(後面會解釋為什麼看起來是全域性的)的方法去載入其他模組。

const util = require(`./util`)
複製程式碼

首先我們來看下關於這個方法,node.js內部是如何定義的:

Module.prototype.require = function () {
  assert(path, `missing path`);
  assert(typeof path === `string`, `path must be a string`);
  // 實際上是呼叫Module._load方法
  return Module._load(path, this, /* isMain */ false);
}

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

  // 獲取檔名
  var filename = Module._resolveFilename(request, parent, isMain);

  // _cache快取的模組
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 如果是nativeModule模組
  if (NativeModule.nonInternalExists(filename)) {
    debug(`load native module %s`, request);
    return NativeModule.require(filename);
  }

  // Don`t call updateChildren(), Module constructor already does.
  // 初始化一個新的module
  var module = new Module(filename, parent);

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

  // 載入模組前,就將這個模組快取起來。注意node.js的模組載入系統是如何避免迴圈依賴的
  Module._cache[filename] = module;

  // 載入module
  tryModuleLoad(module, filename);

  // 將module.exports匯出的內容返回
  return module.exports;
}
複製程式碼

Module._load方法是一個內部的方法,主要是:

  1. 根據你傳入的代表模組路徑的字串來查詢相應的模組路徑;
  2. 根據找到的模組路徑來做快取;
  3. 進而去載入對應的模組。

接下來我們來看下node.js是如何根據傳入的模組路徑字串來查詢對應的模組的:

Module._resolveFilename = function (request, parent, isMain, options) {
  if (NativeModule.nonInternalExists(request)) {
    return request;
  }

  var paths;

  if (typeof options === `object` && options !== null &&
      Array.isArray(options.paths)) {
    ...
  } else {
    // 獲取模組的大致路徑 [parentDir]  | [id, [parentDir]]
    paths = Module._resolveLookupPaths(request, parent, true);
  }

  // look up the filename first, since that`s the cache key.
  // node index.js
  // request = index.js
  // paths = [`/root/foo/bar/index.js`, `/root/foo/bar`]
  var filename = Module._findPath(request, paths, isMain);
  if (!filename) {
    var err = new Error(`Cannot find module `${request}``);
    err.code = `MODULE_NOT_FOUND`;
    throw err;
  }
  return filename;
}
複製程式碼

在這個方法內部,需要呼叫一個內部的方法:Module._resolveLookupPaths,這個方法會依據父模組的路徑獲取所有這個模組可能的路徑:

Module._resolveLookupPaths = function (request, parent, newReturn) {
  ...
}
複製程式碼

這個方法內部有以下幾種情況的處理:

  1. 是啟動模組,即通過node xxx啟動的模組

這個時候node.js會直接獲取到你這個程式執行路徑,並在這個方法當中返回

  1. require(xxx)require一個存在於node_modules中的模組

這個時候會對執行路徑上所有可能存在node_modules的路徑進行遍歷一遍

  1. require(./)require一個相對路徑或者絕對路徑的模組

直接返回父路徑

當拿到需要找尋的路徑後,呼叫Module._findPath方法去查詢對應的檔案路徑。

Module._findPath = function (request, paths, isMain) {
  if (path.isAbsolute(request)) {
    paths = [``];
  } else if (!paths || paths.length === 0) {
    return false;
  }

  // x00 -> null,相當於空字串
  var cacheKey = request + `x00` +
                (paths.length === 1 ? paths[0] : paths.join(`x00`));
  // 路徑的快取
  var entry = Module._pathCache[cacheKey];
  if (entry)
    return entry;

  var exts;
  // 尾部是否帶有/
  var trailingSlash = request.length > 0 &&
                      request.charCodeAt(request.length - 1) === 47/*/*/;

  // For each path
  for (var i = 0; i < paths.length; i++) {
    // Don`t search further if path doesn`t exist
    const curPath = paths[i];   // 當前路徑
    if (curPath && stat(curPath) < 1) continue;
    var basePath = path.resolve(curPath, request);
    var filename;

    // 呼叫internalModuleStat方法來判斷檔案型別
    var rc = stat(basePath);
    // 如果路徑不以/結尾,那麼可能是檔案,也可能是資料夾
    if (!trailingSlash) {
      if (rc === 0) {  // File.  檔案
        if (preserveSymlinks && !isMain) {
          filename = path.resolve(basePath);
        } else {
          filename = toRealPath(basePath);
        }
      } else if (rc === 1) {  // Directory. 當提供的路徑是資料夾的情況下會去這個路徑下找package.json中的main欄位對應的模組的入口檔案
        if (exts === undefined)
          // `.js` `.json` `.node` `.ms`
          exts = Object.keys(Module._extensions);
        // 獲取pkg內部的main欄位對應的值
        filename = tryPackage(basePath, exts, isMain);
      }

      if (!filename) {
        // try it with each of the extensions
        if (exts === undefined)
          exts = Object.keys(Module._extensions);
        filename = tryExtensions(basePath, exts, isMain); // ${basePath}.(js|json|node)等檔案字尾,看是否檔案存在
      }
    }

    // 如果路徑以/結尾,那麼就是資料夾
    if (!filename && rc === 1) {  // Directory.
      if (exts === undefined)
        exts = Object.keys(Module._extensions);
      filename = tryPackage(basePath, exts, isMain) ||
        // try it with each of the extensions at "index"
        tryExtensions(path.resolve(basePath, `index`), exts, isMain);
    }

    if (filename) {
      // Warn once if `.` resolved outside the module dir
      if (request === `.` && i > 0) {
        if (!warned) {
          warned = true;
          process.emitWarning(
            `warning: require(`.`) resolved outside the package ` +
            `directory. This functionality is deprecated and will be removed ` +
            `soon.`,
            `DeprecationWarning`, `DEP0019`);
        }
      }

      // 快取路徑
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
  }
  return false;
}

function tryPackage(requestPath, exts, isMain) {
  var pkg = readPackage(requestPath); // 獲取package.json當中的main欄位

  if (!pkg) return false;

  var filename = path.resolve(requestPath, pkg);  // 解析路徑
  return tryFile(filename, isMain) ||             // 直接判斷這個檔案是否存在
         tryExtensions(filename, exts, isMain) || // 判斷這個分別以js,json,node等字尾結尾的檔案是否存在
         tryExtensions(path.resolve(filename, `index`), exts, isMain);  // 判斷這個分別以 ${filename}/index.(js|json|node)等字尾結尾的檔案是否存在
}
複製程式碼

梳理下上面查詢模組時的一個策略:

  1. require模組的時候,傳入的字串最後一個字元不是/時:
  • 如果是個檔案,那麼直接返回這個檔案的路徑

  • 如果是個資料夾,那麼會找個這個資料夾下是否有package.json檔案,以及這個檔案當中的main欄位對應的路徑(對應原始碼當中的方法為tryPackage):

    • 如果main欄位對應的路徑是一個檔案且存在,那麼就返回這個路徑
    • main欄位對應的路徑對應沒有帶字尾,那麼嘗試使用.js.json.node.ms字尾去載入對應檔案
    • 如果以上2個條件都不滿足,那麼嘗試對應路徑下的index.jsindex.jsonindex.node檔案
  • 如果以上2個方法都沒有找到對應檔案路徑,那麼就對檔案路徑後新增分別新增.js.json.node.ms字尾去載入對應的檔案(對應原始碼當中的方法為tryExtensions)

  1. require模組的時候,傳入的字串最後一個字元是/時,即require的是一個資料夾時:
  • 首先查詢這個資料夾下的package.json檔案中的main欄位對應的路徑,具體的流程方法和上面說的查詢package.json檔案的一致
  • 查詢當前檔案下的index.jsindex.jsonindex.node等檔案

當找到檔案的路徑後就呼叫tryModuleLoad開始載入模組了,這個方法內部實際上是呼叫了模組例項的load方法:

Module.prototype.load = function () {

  ...
  this.filename = filename;
  // 定義module的paths。獲取這個module路徑上所有可能的node_modules路徑
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || `.js`;
  if (!Module._extensions[extension]) extension = `.js`;
  // 開始load這個檔案
  Module._extensions[extension](this, filename);
  this.loaded = true;

  ...
}
複製程式碼

呼叫Module._extension方法去載入不同格式的檔案,就拿js檔案來說:

Module._extensions[`.js`] = function(module, filename) {
  // 首先讀取檔案的文字內容
  var content = fs.readFileSync(filename, `utf8`);
  module._compile(internalModule.stripBOM(content), filename);
};

複製程式碼

內部呼叫了Module.prototype._compile這個方法:

Module.prototype._compile = function (content, filename)) {
  content = internalModule.stripShebang(content);

  // create wrapper function
  // 將原始碼的文字包裹一層
  var wrapper = Module.wrap(content);

  // vm.runInThisContext在一個v8的虛擬機器內部執行wrapper後的程式碼
  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  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 = process.binding(`inspector`).callAndPauseOnStart;
    }
  }
  var dirname = path.dirname(filename);
  // 構造require函式
  var require = internalModule.makeRequireFunction(this);
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result;
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
                              require, this, filename, dirname);
  } else {
    // 開始執行這個函式
    // 傳入的引數依次是 module.exports / require / module / filename / dirname
    result = compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  }
  if (depth === 0) stat.cache = null;
  return result;
}

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

Module.wrapper = [
  `(function (exports, require, module, __filename, __dirname) { `,
  `
});`
];
複製程式碼
  • 通過Module.wrap將原始碼包裹一層(遵循commonJS規範)
  • 通過呼叫vmv8虛擬機器暴露出來的方法來構造一個新的函式
  • 完成函式的呼叫

通過原始碼發現,Module.wrapper在對原始碼文字進行包裹的時候,傳入了5個引數:

  • exports

是對於第三個引數moduleexports屬性的引用

  • require

這個require並非是Module.prototype.require方法,而是通過internalModule.makeRequireFunction重新構造出來的,這個方法內部還是依賴Module.prototype.require方法去載入模組的,同時還對這個require方法做了一些擴充。

  • module

module物件,如果需要向外暴露API供其他模組來使用,需要在module.exports屬性上定義

  • __filename

當前檔案的絕對路徑

  • __dirname

當前檔案的父資料夾的絕對路徑

幾個問題

exports 和 module.exports的關係

特別注意第一個引數和第三引數的聯絡:第一引數是對於第三個引數的exports屬性的引用。一旦將某個模組exports賦值給另外一個新的物件,那麼就斷開了exports屬性和module.exports之間的引用關係,同時在其他模組當中也無法引用在當前模組中通過exports暴露出去的API,對於模組的引用始終是獲取module.exports屬性。

迴圈引用

官方示例:

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
複製程式碼

a模組載入時,需要載入b模組,但是在實際載入a模組之前,就已經將a模組進行的快取,具體參見Module._load方法:

Module._cache[filename] = module;

tryModuleLoad(module, filename);
複製程式碼

因為在載入b模組的過程中再次去載入a模組的時候,這時是直接從快取中獲取a模組匯出的API,此時exports.done的屬性還是false,未被設定為true,只有當b模組被完全載入後,a模組exports屬性才被設定為true

相關文章