Node.js Require源碼粗讀

做者:肖磊javascript

我的主頁:githubjava

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

es2015標準之前,js並無成熟的模塊系統的規範。Node.js爲了彌補這樣一個缺陷,採用了CommonJS規範中所定義的模塊規範,它包括:git

1.requiregithub

require是一個函數,它接收一個模塊的標識符,用以引用其餘模塊暴露出來的APIjson

2.module context緩存

module context規定了一個模塊當中,存在一個require變量,它聽從上面對於這個require函數的定義,一個exports對象,模塊若是須要向外暴露API,即在一個exports的對象上添加屬性。以及一個module objectapp

3.module Identifierside

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) { ',
  '\n});'
];
複製代碼
  • 經過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

相關文章
相關標籤/搜索