Node.js源碼分析之require

前言

require是Node.js中非常重要的一個方法,我們可以使用它在文件中加載其它文件中定義的模塊。本文主要分析了Node.js中使用require加載模塊的主要實現過程,爲了方便理解,對其源碼進行了一定的刪減,去除了部分諸如debug、加載實驗性模塊的代碼。文中選用了當前最新的Node LTS版本(2019年2月) 10.15.1源碼。

源碼分析

在前幾個版本中,require的實現主要位於lib/module.js中,但自從9.11.0開始對其進行了重構,具體的實現移至lib/internal/modules/cjs/loader.js文件中。

require方法即Module模塊的require方法,它主要做了參數的檢查並調用Module模塊的_load方法。Module模塊中存在_load及load兩個方法,需要進行區分。

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

Module模塊的_load方法的主要內容爲檢查cache中是否已經存在該模塊,若已存在則直接從cache中獲取;若cache中不存在,則判斷該模塊是否爲Native模塊,並進行加載。當模塊爲Native模塊,則調用NativeModule.require方法加載,否則將創建一個新的Module實例,調用tryModuleLoad方法加載該模塊內容並將其保存至cache中。在tryModuleLoad方法中,將會調用Module模塊的load方法加載模塊,並根據是否存在錯誤進行相應的處理。

我們將在加載Native模塊章節中具體分析Native模塊的加載過程,本節中繼續以普通的模塊爲主。

Module._load = function(request, parent, isMain) {
  var filename = Module._resolveFilename(request, parent, isMain);

  // 檢查cache中是否已存在該模塊,若已存在則直接從cache中讀取
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 加載Native模塊
  if (NativeModule.nonInternalExists(filename)) {
    return NativeModule.require(filename);
  }

  // 創建一個新的Module,加載並保存至cache中
  var module = new Module(filename, parent);

  Module._cache[filename] = module;

  tryModuleLoad(module, filename);

  return module.exports;
};

function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
    }
  }
}

Module模塊的load方法主要根據需要加載的文件的擴展名判斷並選取對應的加載方法,除此以外它還包括了部分實驗性模塊的處理,本文中不作爲主要內容所以進行了省略。文件的加載方法存放在Module模塊的_extensions數據中,在該文件中主要定義了.js.json.node.mjs四種文件的加載方法。.js.json文件的加載皆爲調用fs模塊的readFileSync方法讀取文件內容後進行對應的處理,而對於.node文件,則使用了process的dlopen方法加載C++擴展。

Module.prototype.load = function(filename) {
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;

  // ...
};

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

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

在加載.js文件時,讀取文件內容後將調用Module模塊的_compile方法,對讀取到的文件內容進行一定的處理,然後使用vm.runInThisContext方法運行腳本,本文中將不再深入探討VM模塊的實現。Module模塊的_compile方法還進行例如設置斷點等處理,有興趣的朋友可自己根據源碼進行深入的學習。

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

  var wrapper = Module.wrap(content);

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

  // ...
};

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

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

至此,便完成了使用require加載模塊的過程,其主要內容就是在判斷緩存後讀取文件內容,然後使用VM模塊運行。接下來我們將繼續看看Native模塊的加載。

讀取Native模塊

Native模塊的加載位於lib/internal/bootstrap/loaders.js文件中。我們在加載Native模塊時,將調用NativeModule模塊的require方法。它的主要過程也和上文中普通模塊的過程相似,先判斷是否已經cache,若未cache則調用NativeModule模塊的compile方法加載模塊並進行cache。

NativeModule.require = function(id) {
  if (id === loaderId) {
    return loaderExports;
  }

  const cached = NativeModule.getCached(id);
  if (cached && (cached.loaded || cached.loading)) {
    return cached.exports;
  }

  if (!NativeModule.exists(id)) {
    // ...
  }

  moduleLoadList.push(`NativeModule ${id}`);

  const nativeModule = new NativeModule(id);

  nativeModule.cache();
  nativeModule.compile();

  return nativeModule.exports;
};

本文也不再繼續深入探索Native模塊的加載過程,有興趣的朋友可根據源碼進行深入的學習。

加載JSON

在Node.js中,可以直接使用require讀取JSON文件的內容,其實現與讀取.js文件時的區別爲讀取後直接調用了JSON.parse方法。

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;
  }
};

結束語

Node幾個大版本中,require的具體實現都有或多或少的改變,但總體仍是以先從cache中獲取,若cache中不存在再讀取文件的思想爲主。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章