node 的模塊運行機制

node 的模塊運行機制簡單瞭解。 涉及大概流程,略過的底層系統區別。javascript

  1. CommonJS 的規範
  2. node 模塊加載過程
  3. 小結

1.CommonJS 的規範

CommonJS 的規範,包括模塊引用模塊定義,模塊標識,3個部分html

模塊引用: 模塊經過require方法來同步加載所依賴的模塊java

模塊定義: 在node中一個文件就是一個模塊,提供exports對象導出當前模塊的方法或變量node

模塊標識: 模塊標識傳遞給require()方法的參數,能夠是按小駝峯(camelCase)命名的字符串,也能夠是文件路徑。c++

1.1.node 模塊中CommonJS 的應用

模塊內容導出兩種方式:git

a.js的內容以下,github

方式一:可將須要導出的變量或函數掛載到 exports 對象的屬性上json

// node.js 每個文件都是一個單獨模塊
// Node對獲取的Javascript文件的內容進行了包裝,以傳入以下變量
console.log(exports, require, module, __filename, __dirname);
// 可將須要導出的變量或函數掛載到 exports 對象的屬性上,
exports.name = 'luoxiaobu';
exports.age = '18'複製代碼

方式二:使用 module.exports 對象總體導出一個變量對象或者函數bootstrap

// node.js 每個文件都是一個單獨模塊
// Node對獲取的Javascript文件的內容進行了包裝,以傳入以下變量
console.log(exports, require, module, __filename, __dirname);
let name = 'luoxiaobu';
let age = '18'
// 使用 module.exports 對象總體導出一個變量對象或者函數,
module.exports = {name,age};複製代碼

模塊的引用的方式: 按照引用模塊的來源來分緩存

// 核心模塊的引入 node本身的模塊
let crypto = require('crypto')

// 用戶本身編寫的模塊引入
let aModule = require('./a.js')
// 第三方,別人實現發佈的模塊(其實也是其餘用戶編寫)
let proxy = require('http-proxy');複製代碼

2.node 模塊加載過程

node.js 每個文件都是一個單獨模塊,每一個模塊都用一個module對象來表示自身。
非 node NativeModule
// 非 node NativeModule
function Module(id = '', parent) {
  this.id = id;
  this.path = path.dirname(id);
  this.exports = {};
  this.parent = parent;
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}複製代碼
NativeModule
// Set up NativeModule.
function NativeModule(id) {
  this.filename = `${id}.js`;
  this.id = id;
  this.exports = {};
  this.module = undefined;
  this.exportKeys = undefined;
  this.loaded = false;
  this.loading = false;
  this.canBeRequiredByUsers = !id.startsWith('internal/');
}複製代碼

2.1 node 模塊加載簡述

加載過程大概流程:(Module._load 加載函數)
代碼略微刪減
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
// `NativeModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
Module._load = function(request, parent, isMain) {
    let relResolveCacheIdentifier;
    if (parent) {
      debug('Module._load REQUEST %s parent: %s', request, parent.id);
      ...
    }
    // 查找文件具體位置
    const filename = Module._resolveFilename(request, parent, isMain);
    // 存在緩存,則不須要再次執行 返回緩存
    const cachedModule = Module._cache[filename];
    if (cachedModule !== undefined) {
      updateChildren(parent, cachedModule, true);
      if (!cachedModule.loaded)
        return getExportsForCircularRequire(cachedModule);
      return cachedModule.exports;
    }
    // 加載node原生模塊,原生模塊loadNativeModule 
    // 若是有 且能被用戶引用 返回 mod.exports(這包括node模塊的編譯建立module對象,將模塊運行結果保存在module對象上)
    const mod = loadNativeModule(filename, request);
    if (mod && mod.canBeRequiredByUsers) return mod.exports;
  
    // 建立一個模塊
    // Don't call updateChildren(), Module constructor already does.
    const module = new Module(filename, parent);
  
    if (isMain) {
      process.mainModule = module;
      module.id = '.';
    }
    // 緩存模塊
    Module._cache[filename] = module;
    if (parent !== undefined) {
      relativeResolveCache[relResolveCacheIdentifier] = filename;
    }
    // 加載執行新的模塊
    module.load(filename);  
      
    return module.exports;
  };複製代碼
node 緩存的是編譯和執行後的對象
相同:
node模塊和非node模塊經歷的過程都是,有執行後的緩存對象,返回緩存對象
沒有執行後的緩存對象,建立module對象,執行模塊,存儲執行後獲得的對象,返回執行後的結果exports
不一樣:
緩存對象不一樣
加載模塊文件方式不一樣

2.2 node 源碼目錄

大概源碼結構:(只標註了部分感興趣的)

咱們能夠看到 node 庫的目錄,其中:
deps:包含了node所依賴的庫,如v8,libuv,zlib 等,
lib:包含了用 javascript 定義的函數和模塊(可能會經過internalBinding調用c++模塊,c++ 模塊實如今目錄src 下),
src:包括了lib 庫對應的C++實現,其中不少 built-in(C++實現) 模塊都在這裏
會有所困惑,js跟c++ 之間的相互調用?
Node.js主要包括這幾個部分,Node Standard Library,Node Bindings,V8,Libuv,架構圖以下:

Node Bindings: 是溝通JS 和 C++的橋樑,將V8 引擎暴露的c++ 接口轉換成JS API

V8: JavaScript的引擎,提供JavaScript運行環境

c++ 模塊的引用大概流程

C++ 和 JS 交互 參考文章:(感興趣能夠了解一下)

2.3 node 模塊分類

// This file creates the internal module & binding loaders used by built-in
// modules. In contrast, user land modules are loaded using
// lib/internal/modules/cjs/loader.js (CommonJS Modules) or
// lib/internal/modules/esm/* (ES Modules).
//
// This file is compiled and run by node.cc before bootstrap/node.js
// was called, therefore the loaders are bootstraped before we start to
// actually bootstrap Node.js. It creates the following objects:
node.cc編譯和運行的node/lib/internal/bootstrap/loaders.js 文件的時候會,建立內部模塊, 綁定內置模塊使用的加載程序。
而用戶的模塊加載運行依靠lib/internal/modules/cjs/loader.js 或者 lib/internal/modules/esm/*
因此node的模塊大體分爲兩類:
  • node的核心模塊
    • node的核心模塊js實現
    • node核心模塊c++實現,js包裹調用c++模塊
  • 第三方模塊,或者用戶本身編寫模塊
    • JavaScript 模塊,咱們開發寫的JavaScript 模
    • JSON 模塊,一個 JSON 文件
    • C/C++ 擴展模塊,使用 C/C++ 編寫,編譯後後綴名爲 .node(感興趣能夠了解動態連接庫)

2.3.1 node的核心模塊

⑴ C++ binding loaders: (對c++核心模塊的引入)
process.binding():舊版C ++綁定加載程序,可從用戶空間訪問,由於它是附加到全局流程對象的對象。這些C ++綁定是使用NODE_BUILTIN_MODULE_CONTEXT_AWARE()建立的,而且其nm_flags設置爲NM_F_BUILTIN。咱們沒法確保這些綁定的穩定性,可是仍然必須時時解決由它們引發的兼容性問題。
process._linkedBinding():在應用程序中添加額外的其餘C ++綁定。能夠使用帶有標誌NM_F_LINKED 的 NODE_MODULE_CONTEXT_AWARE_CPP()建立這些C ++綁定。
internalBinding():私有內部C ++綁定加載程序,(除非經過`require('internal / test / binding')`,不然沒法從用戶區域訪問)。 這些C ++綁定是使用NODE_MODULE_CONTEXT_AWARE_INTERNAL()建立的,其nm_flags設置爲NM_F_INTERNAL。
⑵Internal JavaScript module loader:
該模塊是用於加載 lib/**/*.js 和 deps/**/*.js 中的JavaScript核心模塊的最小模塊系統。
全部核心模塊都經過由 js2c.py 生成的 node_javascript.cc 編譯成 Node 二進制文件,這樣能夠更快地加載它們,而不須要I/O成本。
此類使lib / internal / *,deps / internal / *模塊和internalBinding()在默認狀況下對核心模塊可,並容許核心模塊通require('internal / bootstrap / loaders')來引用自身,即便此文件不是以CommonJS風格編寫的。
核心模塊的加載大概以下:(internalBinding是process.binding的替代能夠簡單這樣理解)

Process.binding / InternalBinding 其實是C++函數,是用於將Node標準庫中C++端和Javascript端鏈接起來的橋樑。

2.3.2 node的非 核心模塊

  • JavaScript 模塊,咱們開發寫的JavaScript 模(或着第三方模塊)
  • JSON 模塊,一個 JSON 文件
  • C/C++ 擴展模塊,使用 C/C++ 編寫,編譯後後綴名爲 .node(感興趣能夠了解動態連接庫)

此類模塊的大概加載流程:

路徑分析

const filename = Module._resolveFilename(request, parent, isMain);複製代碼

是否有緩存

const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }複製代碼

建立module對象

const module = new Module(filename, parent);
// 緩存 module 對象
  Module._cache[filename] = module;複製代碼

文件定位根據後綴編譯執行

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  if (experimentalModules && filename.endsWith('.js')) {
    const pkg = readPackageScope(filename);
    if (pkg && pkg.type === 'module') {
      throw new ERR_REQUIRE_ESM(filename);
    }
  }
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8');

  if (manifest) {
    const moduleURL = pathToFileURL(filename);
    manifest.assertIntegrity(moduleURL, content);
  }

  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) {
  if (manifest) {
    const content = fs.readFileSync(filename);
    const moduleURL = pathToFileURL(filename);
    manifest.assertIntegrity(moduleURL, content);
  }
  // Be aware this doesn't use `content`
  return process.dlopen(module, path.toNamespacedPath(filename));
};複製代碼

返回module.exports 對象。

3.總結

node 的模塊運行機制簡單瞭解。 涉及大概流程,略過的底層系統區別。

文章整理了相關資料,記錄了部分實踐和本身的理解,理解不許確之處,還請教正。歡迎一塊兒討論學習。

參考資料:


node 源碼

相關文章
相關標籤/搜索