<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Node模塊加載機制,一文帶你徹底理解

          共 7048字,需瀏覽 15分鐘

           ·

          2021-04-30 19:54


          一.require()時發(fā)生了什么?

          Node.js 中,模塊加載過程分為 5 步:

          1. 路徑解析(Resolution):根據(jù)模塊標識找出對應模塊(入口)文件的絕對路徑

          2. 加載(Loading):如果是 JSON 或 JS 文件,就把文件內(nèi)容讀入內(nèi)存。如果是內(nèi)置的原生模塊,將其共享庫動態(tài)鏈接到當前 Node.js 進程

          3. 包裝(Wrapping):將文件內(nèi)容(JS 代碼)包進一個函數(shù),建立模塊作用域,exports, require, module等作為參數(shù)注入

          4. 執(zhí)行(Evaluation):傳入?yún)?shù),執(zhí)行包裝得到的函數(shù)

          5. 緩存(Caching):函數(shù)執(zhí)行完畢后,將module緩存起來,并把module.exports作為require()的返回值返回

          其中,模塊標識(Module Identifiers)就是傳入require(id)的第一個字符串參數(shù)id,例如require('./myModule')中的'./myModule'無需指定后綴名(但帶上也無礙)

          對于.、../開頭的文件路徑,嘗試當做文件、目錄來匹配,具體過程如下:

          1. 若路徑存在并且是個文件,就當做 JS 代碼來加載(無論文件后綴名是什么,require(./myModule.abcd)完全正確)

          2. 若不存在,依次嘗試拼上.js、.json.node(Node.js 支持的二進制擴展)后綴名

          3. 如果路徑存在并且是個文件夾,就在該目錄下找package.json,取其main字段,并加載指定的模塊(相當于一次重定向)

          4. 如果沒有package.json,就依次嘗試index.js、index.jsonindex.node

          對于模塊標識不是文件路徑的,先看是不是 Node.js 原生模塊(fspath等)。如果不是,就從當前目錄開始,逐級向上在各個node_modules下找,一直找到頂層的/node_modules,以及一些全局目錄:

          • NODE_PATH環(huán)境變量中指定的位置

          • 默認的全局目錄:$HOME/.node_modules、$HOME/.node_libraries$PREFIX/lib/node

          P.S.關(guān)于全局目錄的更多信息,見Loading from the global folders

          找到模塊文件后,讀取內(nèi)容,并包一層函數(shù):

          (function(exports, require, module, __filename, __dirname) {
          // Module code actually lives in here
          });

          (摘自The module wrapper)

          執(zhí)行時從外部注入這些模塊變量(exports, require, module, __filename, __dirname),模塊導出的東西通過module.exports帶出來,并將整個module對象緩存起來,最后返回require()結(jié)果

          循環(huán)依賴

          特殊的,模塊之間可能會出現(xiàn)循環(huán)依賴,對此,Node.js 的處理策略非常簡單:

          // module1.js
          exports.a = 1;
          require('./module2');
          exports.b = 2;
          exports.c = 3;

          // module2.js
          const module1 = require('./module1');
          console.log('module1 is partially loaded here', module1);

          module1.js執(zhí)行中引用了module2.js,module2又引了module1,此時module1尚未加載完(exports.b = 2; exports.c = 3;還沒執(zhí)行)。而在 Node.js 里,只加載了一部分的模塊也可以正常引用

          When there are circular require() calls, a module might not have finished executing when it is returned.

          所以module1.js執(zhí)行結(jié)果是:

          module1 is partially loaded here { a: 1 }

          P.S.關(guān)于循環(huán)引用的更多信息,見Cycles

          二.Node.js 內(nèi)部是怎么實現(xiàn)的?

          實現(xiàn)上,模塊加載的絕大多數(shù)工作都是由module模塊來完成的:

          const Module = require('module');
          console.log(Module);

          Module是個函數(shù)/類:

          function Module(id = '', parent) {
          this.id = id;
          this.path = path.dirname(id);
          // 即module.exports
          this.exports = {};
          this.parent = parent;
          updateChildren(parent, this, false);
          this.filename = null;
          this.loaded = false;
          this.children = [];
          }

          每加載一個模塊都創(chuàng)建一個Module實例,模塊文件執(zhí)行完后,該實例仍然保留,模塊導出的東西依附于Module實例存在

          模塊加載的所有工作都是由module原生模塊來完成的,包括Module._load、Module.prototype._compile

          Module._load

          Module._load()負責加載新模塊、管理緩存,具體如下:

          Module._load = function(request, parent, isMain) {
          // 0.解析模塊路徑
          const filename = Module._resolveFilename(request, parent, isMain);
          // 1.優(yōu)先找緩存 Module._cache
          const cachedModule = Module._cache[filename];
          // 2.嘗試匹配原生模塊
          const mod = loadNativeModule(filename, request, experimentalModules);
          // 3.未命中緩存,也沒匹配到原生模塊,就創(chuàng)建一個新的 Module 實例
          const module = new Module(filename, parent);
          // 4.把新實例緩存起來
          Module._cache[filename] = module;
          // 5.加載模塊
          module.load(filename);
          // 6.如果加載/執(zhí)行出錯了,就刪掉緩存
          if (threw) {
          delete Module._cache[filename];
          }
          // 7.返回 module.exports
          return module.exports;
          };

          Module.prototype.load = function(filename) {
          // 0.判定模塊類型
          const extension = findLongestRegisteredExtension(filename);
          // 1.按類型加載模塊內(nèi)容
          Module._extensions[extension](this, filename);
          };

          支持的類型有.js、.json、.node3 種:

          // Native extension for .js
          Module._extensions['.js'] = function(module, filename) {
          // 1.讀取JS文件內(nèi)容
          const content = fs.readFileSync(filename, 'utf8');
          // 2.包裝、執(zhí)行
          module._compile(content, filename);
          };

          // Native extension for .json
          Module._extensions['.json'] = function(module, filename) {
          // 1.讀取JSON文件內(nèi)容
          const content = fs.readFileSync(filename, 'utf8');
          // 2.直接JSON.parse()完事
          module.exports = JSONParse(stripBOM(content));
          };

          // Native extension for .node
          Module._extensions['.node'] = function(module, filename) {
          // 動態(tài)加載共享庫
          return process.dlopen(module, path.toNamespacedPath(filename));
          };

          P.S.process.dlopen具體見process.dlopen(module, filename[, flags])

          Module.prototype._compile

          Module.prototype._compile = function(content, filename) {
          // 1.包一層函數(shù)
          const compiledWrapper = wrapSafe(filename, content, this);
          // 2.把要注入的參數(shù)準備好
          const dirname = path.dirname(filename);
          const require = makeRequireFunction(this, redirects);
          const exports = this.exports;
          const thisValue = exports;
          const module = this;
          // 3.注入?yún)?shù)、執(zhí)行
          compiledWrapper.call(thisValue, exports, require, module, filename, dirname);
          };

          包裝部分的實現(xiàn)如下:

          function wrapSafe(filename, content, cjsModuleInstance) {
          let compiled = compileFunction(
          content,
          filename,
          0,
          0,
          undefined,
          false,
          undefined,
          [],
          [
          'exports',
          'require',
          'module',
          '__filename',
          '__dirname',
          ]
          );

          return compiled.function;
          }

          P.S.模塊加載的完整實現(xiàn)見node/lib/internal/modules/cjs/loader.js

          三.知道這些有什么用?

          知道了模塊的加載機制,在一些需要擴展篡改加載邏輯的場景很有用,比如用來實現(xiàn)虛擬模塊、模塊別名等

          虛擬模塊

          比如,VS Code 插件通過require('vscode')來訪問插件 API:

          // The module 'vscode' contains the VS Code extensibility API
          import * as vscode from 'vscode';

          vscode模塊實際上是不存在的,是個運行時擴展出來的虛擬模塊:

          // ref: src/vs/workbench/api/node/extHost.api.impl.ts
          function defineAPI() {
          const node_module = <any>require.__$__nodeRequire('module');
          const original = node_module._load;
          // 1.劫持 Module._load
          node_module._load = function load(request, parent, isMain) {
          if (request !== 'vscode') {
          return original.apply(this, arguments);
          }

          // 2.注入虛擬模塊 vscode
          // get extension id from filename and api for extension
          const ext = extensionPaths.findSubstr(parent.filename);
          let apiImpl = extApiImpl.get(ext.id);
          if (!apiImpl) {
          apiImpl = factory(ext);
          extApiImpl.set(ext.id, apiImpl);
          }
          return apiImpl;
          };
          }

          具體見API 注入機制及插件啟動流程_VSCode 插件開發(fā)筆記 2,這里不再贅述

          模塊別名

          類似的,可以通過重寫Module._resolveFilename來實現(xiàn)模塊別名,比如把proj/src中的@lib/my-module模塊引用映射到proj/lib/my-module

          // src/index.js
          require('./patchModule');

          const myModule = require('@lib/my-module');
          console.log(myModule);

          patchModule具體實現(xiàn)如下:

          const Module = require('module');
          const path = require('path');

          const _resolveFilename = Module._resolveFilename;
          Module._resolveFilename = function(request) {
          const args = Array.from(arguments);
          // 別名映射
          const LIB_PREFIX = '@lib/';
          if (request.startsWith(LIB_PREFIX)) {
          console.log(request);
          request = path.resolve(__dirname, '../' + request.slice(1));
          args[0] = request;
          console.log(` => ${request}`);
          }
          return _resolveFilename.apply(null, args);
          }

          P.S.當然,一般不需要這樣做,可以通過Webpack等構(gòu)建工具來完成

          清掉緩存

          默認 Node.js 模塊加載過就有緩存,而有些時候可能想要禁掉緩存,強制重新加載一個模塊,比如想要讀取能被用戶頻繁修改的 JS 文件(如webpack.config.js

          此時可以手動刪掉掛在require.cache身上的module.exports緩存:

          delete require.cache[require.resolve('./b.js')]

          然而,如果b.js還引用了其它外部(非原生)模塊,也需要一并刪除

          const mod = require.cache[require.resolve('./b.js')];
          // 把引用樹上所有模塊緩存全都刪掉
          (function traverse(mod) {
          mod.children.forEach((child) => {
          traverse(child);
          });

          console.log('decache ' + mod.id);
          delete require.cache[mod.id];
          }(mod));

          P.S.或者采用decache模塊

          參考資料

          • Node.js, TC-39, and Modules:以及譯文

          • The Node.js Way – How require() Actually Works

          • Requiring modules in Node.js: Everything you need to know

          • Deep Dive Into Node.js Module Architecture

          • node.js require() cache – possible to invalidate?

          聯(lián)系我      

          如果心中仍有疑問,請查看原文并留下評論噢。(特別要緊的問題,可以直接微信聯(lián)系 ayqywx


          瀏覽 146
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  性无码一区二区三区在线观看 | 乱伦免费视频中文字幕 | 大逼色网站 | 射精视频在线观看 | 台湾成人综合网 |