摘要:在中,要說如果有幾乎會在每一個文件都要用到的一個全局函數(shù)和一個全局對象,那應該是非和莫屬了。它們是模塊機制的基石。若仍未返回,則為指定的模塊路徑依次加上,和,判斷是否存在,若存在則返回拼接后的路徑。
在 Node.js 中,要說如果有幾乎會在每一個文件都要用到的一個全局函數(shù)和一個全局對象,那應該是非 require 和 module.exports 莫屬了。它們是 Node.js 模塊機制的基石。大家在使用它們享受模塊化的好處時,有時也不禁好奇:
為何它倆使用起來像是全局函數(shù)/對象,卻在 global 對象下訪問不到它們?
"use strict" console.log(require) // Function console.log(module) // Object console.log(global.require) // undefined console.log(global.module) // undefined
這兩個“類全局”對象是在什么時候,怎么生成的?
當 require 一個目錄時,Node.js 是如何替我們找到具體該執(zhí)行的文件的?
模塊內(nèi)的代碼具體是以何種方式被執(zhí)行的?
循環(huán)依賴了怎么辦?
讓我們從 Node.js 項目的 lib/module.js 中的代碼里,細細看一番,一個文件被 require 后,具體發(fā)生的故事,從而來解答上面這些問題。
一個文件被 require 后所發(fā)生的故事當我們在命令行中敲下:
node ./index.js
之后,src/node.cc 中的 node::LoadEnvironment 函數(shù)會被調(diào)用,在該函數(shù)內(nèi)則會接著調(diào)用 src/node.js 中的代碼,并執(zhí)行 startup 函數(shù):
// src/node.js // ... function startup() { // ... Module.runMain(); } // lib/module.js // ... Module.runMain = function() { // ... Module._load(process.argv[1], null, true); // ... };
所以,最后會執(zhí)行到 Module._load(process.argv[1], null, true); 這條語句來加載模塊,不過其實,這個Module._load在require函數(shù)的代碼中也會被調(diào)用:
// lib/module.js // ... Module.prototype.require = function(path) { assert(path, "missing path"); assert(typeof path === "string", "path must be a string"); return Module._load(path, this, false); };
所以說,當我們在命令行中敲下 node ./index.js,某種意義上,可以說隨后 Node.js 的表現(xiàn)即為立刻進行一次 require , 即:
require("./index.js")
隨后的步驟就是 require 一個普通模塊了,讓我們繼續(xù)往下看,Module._load 方法做的第一件事,便是調(diào)用內(nèi)部方法 Module._resolveFilename ,而該內(nèi)部方法在進行了一些參數(shù)預處理后,最終會調(diào)用 Module._findPath 方法,來得到需被導入模塊的完整路徑,讓我們從代碼中來總結(jié)出它的路徑分析規(guī)則:
// lib/module.js // ... Module._findPath = function(request, paths) { // 優(yōu)先取緩存 var cacheKey = JSON.stringify({request: request, paths: paths}); if (Module._pathCache[cacheKey]) { return Module._pathCache[cacheKey]; } // ... for (var i = 0, PL = paths.length; i < PL; i++) { if (!trailingSlash) { const rc = stat(basePath); if (rc === 0) { // 若是文件. filename = toRealPath(basePath); } else if (rc === 1) { // 若是目錄 filename = tryPackage(basePath, exts); } if (!filename) { // 帶上 .js .json .node 后綴進行嘗試 filename = tryExtensions(basePath, exts); } } if (!filename) { filename = tryPackage(basePath, exts); } if (!filename) { // 嘗試 index.js index.json index.node filename = tryExtensions(path.resolve(basePath, "index"), exts); } if (filename) { // ... Module._pathCache[cacheKey] = filename; return filename; } } return false; }; function tryPackage(requestPath, exts) { var pkg = readPackage(requestPath); // 獲取 package.json 中 main 屬性的值 // ... return tryFile(filename) || tryExtensions(filename, exts) || tryExtensions(path.resolve(filename, "index"), exts); }
代碼中的條件判斷十分清晰,讓我們來總結(jié)一下:
若模塊的路徑不以 / 結(jié)尾,則先檢查該路徑是否真實存在:
若存在且為一個文件,則直接返回文件路徑作為結(jié)果。
若存在且為一個目錄,則嘗試讀取該目錄下的 package.json 中 main 屬性所指向的文件路徑。
判斷該文件路徑是否存在,若存在,則直接作為結(jié)果返回。
嘗試在該路徑后依次加上 .js , .json 和 .node 后綴,判斷是否存在,若存在則返回加上后綴后的路徑。
嘗試在該路徑后依次加上 index.js , index.json 和 index.node,判斷是否存在,若存在則返回拼接后的路徑。
若仍未返回,則為指定的模塊路徑依次加上 .js , .json 和 .node 后綴,判斷是否存在,若存在則返回加上后綴后的路徑。
若模塊以 / 結(jié)尾,則嘗試讀取該目錄下的 package.json 中 main 屬性所指向的文件路徑。
判斷該文件路徑是否存在,若存在,則直接作為結(jié)果返回。
嘗試在該路徑后依次加上 .js , .json 和 .node 后綴,判斷是否存在,若存在則返回加上后綴后的路徑。
嘗試在該路徑后依次加上 index.js , index.json 和 index.node,判斷是否存在,若存在則返回拼接后的路徑。
若仍未返回,則為指定的模塊路徑依次加上 index.js , index.json 和 index.node,判斷是否存在,若存在則返回拼接后的路徑。
在取得了模塊的完整路徑后,便該是執(zhí)行模塊了,我們以執(zhí)行 .js 后綴的 JavaScript 模塊為例。首先 Node.js 會通過 fs.readFileSync 方法,以 UTF-8 的格式,將 JavaScript 代碼以字符串的形式讀出,傳遞給內(nèi)部方法 module._compile,在這個內(nèi)部方法里,則會調(diào)用 NativeModule.wrap 方法,將我們的模塊代碼包裹在一個函數(shù)中:
// src/node.js // ... NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ "(function (exports, require, module, __filename, __dirname) { ", " });" ];
所以,這便解答了我們之前提出的,在 global 對象下取不到它們的問題,因為它們是以包裹在外的函數(shù)的參數(shù)的形式傳遞進來的。所以順便提一句,我們平常在文件的頂上寫的 use strict ,其實最終聲明的并不是 script-level 的嚴格模式,而都是 function-level 的嚴格模式。
最后一步, Node.js 會使用 vm.runInThisContext 執(zhí)行這個拼接完畢的字符串,取得一個 JavaScript 函數(shù),最后帶著對應的對象參數(shù)執(zhí)行它們,并將賦值在 module.exports 上的對象返回:
// lib/module.js // ... Module.prototype._compile = function(content, filename) { // ... var compiledWrapper = runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); // ... const args = [this.exports, require, this, filename, dirname]; const result = compiledWrapper.apply(this.exports, args); // ... };
至此,一個同步的 require 操作便圓滿結(jié)束啦。
循環(huán)依賴通過上文我們已經(jīng)可以知道,在 Module._load 內(nèi)部方法里 Node.js 在加載模塊之前,首先就會把傳模塊內(nèi)的 module 對象的引用給緩存起來(此時它的 exports 屬性還是一個空對象),然后執(zhí)行模塊內(nèi)代碼,在這個過程中漸漸為 module.exports 對象附上該有的屬性。所以當 Node.js 這么做時,出現(xiàn)循環(huán)依賴的時候,僅僅只會讓循環(huán)依賴點取到中間值,而不會讓 require 死循環(huán)卡住。一個經(jīng)典的例子:
// a.js "use strict" console.log("a starting") exports.done = false var b = require("./b") console.log(`in a, b.done=${b.done}`) exports.done = true console.log("a done")
// b.js "use strict" console.log("b start") exports.done = false let a = require("./a") console.log(`in b, a.done=${a.done}`) exports.done = true console.log("b done")
// main.js "use strict" console.log("main start") let a = require("./a") let b = require("./b") console.log(`in main, a.done=${a.done}, b.done=${b.done}`)
執(zhí)行 node main.js ,打印:
main start a starting b start in b, a.done=false => 循環(huán)依賴點取到了中間值 b done in a, b.done=true a done in main, a.done=true, b.done=true最后
由于 Node.js 中的模塊導入和 ES6 規(guī)范中的不同,它的導入過程是同步的。所以實現(xiàn)起來會方便許多,代碼量同樣也不多。十分推薦大家閱讀一下完整的實現(xiàn)。
參考:
https://github.com/nodejs/node/blob/v5.x/lib/module.js
https://github.com/nodejs/node/blob/v5.x/src/node.js
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/87731.html
摘要:要想讓模塊再次運行,必須清除緩存。用戶自己編寫的模塊,稱為文件模塊。并且和指向了同一個模塊對象。模塊路徑這是在定位文件模塊的具體文件時指定的查找策略,具體表現(xiàn)為一個路徑組成的數(shù)組。 前言 Node應用是由模塊組成的,Node遵循了CommonJS的模塊規(guī)范,來隔離每個模塊的作用域,使每個模塊在它自身的命名空間中執(zhí)行。 CommonJS規(guī)范的主要內(nèi)容: 模塊必須通過 module.exp...
摘要:全局范圍生效,不需要。解析本地路徑首先來為你介紹對象,可以先在控制臺中看一下每一個模塊都有屬性來唯一標示它。通常是文件的完整路徑,但是在控制臺中一般顯示成。 showImg(https://segmentfault.com/img/remote/1460000009060869?w=1794&h=648); 本文作者:Jacob Beltran 編譯:胡子大哈 翻譯原文:http:...
摘要:使用要給項目構(gòu)建接入動態(tài)鏈接庫的思想,需要完成以下事情把網(wǎng)頁依賴的基礎(chǔ)模塊抽離出來,打包到一個個單獨的動態(tài)鏈接庫中去。接入已經(jīng)內(nèi)置了對動態(tài)鏈接庫的支持,需要通過個內(nèi)置的插件接入,它們分別是插件用于打包出一個個單獨的動態(tài)鏈接庫文件。 webpack優(yōu)化 查看所有文檔頁面:全棧開發(fā),獲取更多信息。原文鏈接:webpack優(yōu)化,原文廣告模態(tài)框遮擋,閱讀體驗不好,所以整理成本文,方便查找。 ...
摘要:概述相信很多的人,每天在終端不止一遍的執(zhí)行著這條命令,對于很多人來說,它就像一個黑盒,并不知道背后到底發(fā)生了什么,本文將會為大家揭開這個神秘的面紗,由于本人水平有限,所以只是講一個大概其,主要關(guān)注的過程就是模塊的初始化,和的部分基本沒有深入 概述 相信很多的人,每天在終端不止一遍的執(zhí)行著node這條命令,對于很多人來說,它就像一個黑盒,并不知道背后到底發(fā)生了什么,本文將會為大家揭開這個...
閱讀 482·2019-08-30 15:44
閱讀 897·2019-08-30 10:55
閱讀 2729·2019-08-29 15:16
閱讀 924·2019-08-29 13:17
閱讀 2801·2019-08-26 13:27
閱讀 568·2019-08-26 11:53
閱讀 2119·2019-08-23 18:31
閱讀 1882·2019-08-23 18:23