Commonjs規范及Node模塊實現

前面的話

  Node在實現中并非完全按照CommonJS規范實現,而是對模塊規范進行了一定的取舍,同時也增加了少許自身需要的特性。本文將詳細介紹NodeJS的模塊實現

?

引入

  nodejs是區別于javascript的,在javascript中的頂層對象是window,而在node中的頂層對象是global

  [注意]實際上,javascript也存在global對象,只是其并不對外訪問,而使用window對象指向global對象而已

  在javascript中,通過var a = 100;是可以通過window.a來得到100的

  但在nodejs中,是不能通過global.a來訪問,得到的是undefined

  這是因為var a = 100;這個語句中的變量a,只是模塊范圍內的變量a,而不是global對象下的a

  在nodejs中,一個文件就是一個模塊,每個模塊都有自己的作用域。使用var來聲明的一個變量,它并不是全局的,而是屬于當前模塊下

  如果要在全局作用域下聲明變量,則如下所示

?

概述

  Node中模塊分為兩類:一類是Node提供的模塊,稱為核心模塊;另一類是用戶編寫的模塊,稱為文件模塊

  核心模塊部分在Node源代碼的編譯過程中,編譯進了二進制執行文件。在Node進程啟動時,部分核心模塊就被直接加載進內存中,所以這部分核心模塊引入時,文件定位和編譯執行這兩個步驟可以省略掉,并且在路徑分析中優先判斷,所以它的加載速度是最快的

  文件模塊則是在運行時動態加載,需要完整的路徑分析、文件定位、編譯執行過程,速度比核心模塊慢

  接下來,我們展開詳細的模塊加載過程

?

模塊加載

  在javascript中,加載模塊使用script標簽即可,而在nodejs中,如何在一個模塊中,加載另一個模塊呢?

  使用require()方法來引入

【緩存加載】

  再展開介紹require()方法的標識符分析之前,需要知道,與前端瀏覽器會緩存靜態腳本文件以提高性能一樣,Node對引入過的模塊都會進行緩存,以減少二次引入時的開銷。不同的地方在于,瀏覽器僅僅緩存文件,而Node緩存的是編譯和執行之后的對象

  不論是核心模塊還是文件模塊,require()方法對相同模塊的二次加載都一律采用緩存優先的方式,這是第一優先級的。不同之處在于核心模塊的緩存檢查先于文件模塊的緩存檢查

【標識符分析】

  require()方法接受一個標識符作為參數。在Node實現中,正是基于這樣一個標識符進行模塊查找的。模塊標識符在Node中主要分為以下幾類:[1]核心模塊,如http、fs、path等;[2].或..開始的相對路徑文件模塊;[3]以/開始的絕對路徑文件模塊;[4]非路徑形式的文件模塊,如自定義的connect模塊

  根據參數的不同格式,require命令去不同路徑尋找模塊文件

  1、如果參數字符串以“/”開頭,則表示加載的是一個位于絕對路徑的模塊文件。比如,require('/home/marco/foo.js')將加載/home/marco/foo.js

  2、如果參數字符串以“./”開頭,則表示加載的是一個位于相對路徑(跟當前執行腳本的位置相比)的模塊文件。比如,require('./circle')將加載當前腳本同一目錄的circle.js

  3、如果參數字符串不以“./“或”/“開頭,則表示加載的是一個默認提供的核心模塊(位于Node的系統安裝目錄中),或者一個位于各級node_modules目錄的已安裝模塊(全局安裝或局部安裝)

  [注意]如果是當前路徑下的文件模塊,一定要以./開頭,否則nodejs會試圖去加載核心模塊,或node_modules內的模塊?

//a.js
console.log('aaa');//b.js
require('./a');//'aaa'
require('a');//報錯

【文件擴展名分析】

  require()在分析標識符的過程中,會出現標識符中不包含文件擴展名的情況。CommonJS模塊規范也允許在標識符中不包含文件擴展名,這種情況下,Node會先查找是否存在沒有后綴的該文件,如果沒有,再按.js、.json、.node的次序補足擴展名,依次嘗試

  在嘗試的過程中,需要調用fs模塊同步阻塞式地判斷文件是否存在。因為Node是單線程的,所以這里是一個會引起性能問題的地方。小訣竅是:如果是.node和.json文件,在傳遞給require()的標識符中帶上擴展名,會加快一點速度。另一個訣竅是:同步配合緩存,可以大幅度緩解Node單線程中阻塞式調用的缺陷

【目錄分析和包】

  在分析標識符的過程中,require()通過分析文件擴展名之后,可能沒有查找到對應文件,但卻得到一個目錄,這在引入自定義模塊和逐個模塊路徑進行查找時經常會出現,此時Node會將目錄當做一個包來處理

  在這個過程中,Node對CommonJS包規范進行了一定程度的支持。首先,Node在當前目錄下查找package.json(CommonJS包規范定義的包描述文件),通過JSON.parse()解析出包描述對象,從中取出main屬性指定的文件名進行定位。如果文件名缺少擴展名,將會進入擴展名分析的步驟

  而如果main屬性指定的文件名錯誤,或者壓根沒有package.json文件,Node會將index當做默認文件名,然后依次查找index.js、index.json、index.node

  如果在目錄分析的過程中沒有定位成功任何文件,則自定義模塊進入下一個模塊路徑進行查找。如果模塊路徑數組都被遍歷完畢,依然沒有查找到目標文件,則會拋出查找失敗的異常

?

訪問變量

  如何在一個模塊中訪問另外一個模塊中定義的變量呢??

【global】

  最容易想到的方法,把一個模塊定義的變量復制到全局環境global中,然后另一個模塊訪問全局環境即可

//a.js
var a = 100;
global.a = a;//b.js
require('./a');
console.log(global.a);//100

  這種方法雖然簡單,但由于會污染全局環境,不推薦使用

【module】

  而常用的方法是使用nodejs提供的模塊對象Module,該對象保存了當前模塊相關的一些信息

function Module(id, parent) {this.id = id;this.exports = {};this.parent = parent;if (parent && parent.children) {parent.children.push(this);}this.filename = null;this.loaded = false;this.children = [];
}
module.id 模塊的識別符,通常是帶有絕對路徑的模塊文件名。
module.filename 模塊的文件名,帶有絕對路徑。
module.loaded 返回一個布爾值,表示模塊是否已經完成加載。
module.parent 返回一個對象,表示調用該模塊的模塊。
module.children 返回一個數組,表示該模塊要用到的其他模塊。
module.exports 表示模塊對外輸出的值。

【exports】

  module.exports屬性表示當前模塊對外輸出的接口,其他文件加載該模塊,實際上就是讀取module.exports變量

//a.js
var a = 100;
module.exports.a = a;//b.js
var result = require('./a');
console.log(result);//'{ a: 100 }'

  為了方便,Node為每個模塊提供一個exports變量,指向module.exports。造成的結果是,在對外輸出模塊接口時,可以向exports對象添加方法

console.log(module.exports === exports);//true

  [注意]不能直接將exports變量指向一個值,因為這樣等于切斷了exportsmodule.exports的聯系

?

模塊編譯

  編譯和執行是模塊實現的最后一個階段。定位到具體的文件后,Node會新建一個模塊對象,然后根據路徑載入并編譯。對于不同的文件擴展名,其載入方法也有所不同,具體如下所示

  js文件——通過fs模塊同步讀取文件后編譯執行

  node文件——這是用C/C++編寫的擴展文件,通過dlopen()方法加載最后編譯生成的文件

  json文件——通過fs模塊同步讀取文件后,用JSON.parse()解析返回結果

  其余擴展名文件——它們都被當做.js文件載入

  每一個編譯成功的模塊都會將其文件路徑作為索引緩存在Module._cache對象上,以提高二次引入的性能

  根據不同的文件擴展名,Node會調用不同的讀取方式,如.json文件的調用如下:

// Native extension for .json
Module._extensions['.json'] = function(module, filename) {var content = NativeModule.require('fs').readFileSync(filename, 'utf8'); try {module.exports = JSON.parse(stripBOM(content));} catch (err) {err.message = filename + ': ' + err.message;throw err;}
};

  其中,Module._extensions會被賦值給require()的extensions屬性,所以通過在代碼中訪問require.extensions可以知道系統中已有的擴展加載方式。編寫如下代碼測試一下:

console.log(require.extensions);

  得到的執行結果如下:

{ '.js': [Function], '.json': [Function], '.node': [Function] }

  在確定文件的擴展名之后,Node將調用具體的編譯方式來將文件執行后返回給調用者

【JavaScript模塊的編譯】

  回到CommonJS模塊規范,我們知道每個模塊文件中存在著require、exports、module這3個變量,但是它們在模塊文件中并沒有定義,那么從何而來呢?甚至在Node的API文檔中,我們知道每個模塊中還有filename、dirname這兩個變量的存在,它們又是從何而來的呢?如果我們把直接定義模塊的過程放諸在瀏覽器端,會存在污染全局變量的情況

  事實上,在編譯的過程中,Node對獲取的JavaScript文件內容進行了頭尾包裝。在頭部添加了(function(exports, require, module, filename, dirname) {\n,在尾部添加了\n});

  一個正常的JavaScript文件會被包裝成如下的樣子

(function (exports, require, module,  filename,  dirname) {var math = require('math');exports.area = function (radius) {return Math.PI * radius * radius;};
});

  這樣每個模塊文件之間都進行了作用域隔離。包裝之后的代碼會通過vm原生模塊的runInThisContext()方法執行(類似eval,只是具有明確上下文,不污染全局),返回一個具體的function對象。最后,將當前模塊對象的exports屬性、require()方法、module(模塊對象自身),以及在文件定位中得到的完整文件路徑和文件目錄作為參數傳遞給這個function()執行

  這就是這些變量并沒有定義在每個模塊文件中卻存在的原因。在執行之后,模塊的exports屬性被返回給了調用方。exports屬性上的任何方法和屬性都可以被外部調用到,但是模塊中的其余變量或屬性則不可直接被調用

  至此,require、exports、module的流程已經完整,這就是Node對CommonJS模塊規范的實現

【C/C++模塊的編譯】

  Node調用process.dlopen()方法進行加載和執行。在Node的架構下,dlopen()方法在Windows和*nix平臺下分別有不同的實現,通過libuv兼容層進行了封裝

  實際上,.node的模塊文件并不需要編譯,因為它是編寫C/C++模塊之后編譯生成的,所以這里只有加載和執行的過程。在執行的過程中,模塊的exports對象與.node模塊產生聯系,然后返回給調用者

  C/C++模塊給Node使用者帶來的優勢主要是執行效率方面的,劣勢則是C/C++模塊的編寫門檻比JavaScript高

【JSON文件的編譯】

  .json文件的編譯是3種編譯方式中最簡單的。Node利用fs模塊同步讀取JSON文件的內容之后,調用JSON.parse()方法得到對象,然后將它賦給模塊對象的exports,以供外部調用

  JSON文件在用作項目的配置文件時比較有用。如果你定義了一個JSON文件作為配置,那就不必調用fs模塊去異步讀取和解析,直接調用require()引入即可。此外,你還可以享受到模塊緩存的便利,并且二次引入時也沒有性能影響

?

CommonJS

  在介紹完Node的模塊實現之后,回過頭來再學習下CommonJS規范,相對容易理解

  CommonJS規范的提出,主要是為了彌補當前javascript沒有標準的缺陷,使其具備開發大型應用的基礎能力,而不是停留在小腳本程序的階段

  CommonJS對模塊的定義十分簡單,主要分為模塊引用、模塊定義和模塊標識3個部分

【模塊引用】

var math = require('math');

  在CommonJS規范中,存在require()方法,這個方法接受模塊標識,以此引入一個模塊的API到當前上下文中

【模塊定義】

  在模塊中,上下文提供require()方法來引入外部模塊。對應引入的功能,上下文提供了exports對象用于導出當前模塊的方法或者變量,并且它是唯一導出的出口。在模塊中,還存在一個module對象,它代表模塊自身,而exports是module的屬性。在Node中,一個文件就是一個模塊,將方法掛載在exports對象上作為屬性即可定義導出的方式:

// math.js
exports.add = function () {var sum = 0, i = 0,args = arguments, l = args.length;while (i < l) {sum += args[i++];}return sum;
};

  在另一個文件中,我們通過require()方法引入模塊后,就能調用定義的屬性或方法了

// program.js
var math = require('math');
exports.increment = function (val) {return math.add(val, 1);
};

【模塊標識】

  模塊標識其實就是傳遞給require()方法的參數,它必須是符合小駝峰命名的字符串,或者以.、..開頭的相對路徑,或者絕對路徑。它可以沒有文件名后綴.js

  模塊的定義十分簡單,接口也十分簡潔。它的意義在于將類聚的方法和變量等限定在私有的作用域中,同時支持引入和導出功能以順暢地連接上下游依賴。每個模塊具有獨立的空間,它們互不干擾,在引用時也顯得干凈利落

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/541389.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/541389.shtml
英文地址,請注明出處:http://en.pswp.cn/news/541389.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

thinkphp3 php jwt,ThinkPHP5 使用 JWT 進行加密

- 使用 Composer安裝此擴展- 代碼示例<?php /*** [InterCommon-接口公用]* Author RainCyan* DateTime 2019-08-12T16:38:080800*/namespace app\hladmin\controller;use think\Controller;use \Firebase\JWT\JWT;class InterCommonController extends Controller {private…

數據管理與商業智能_商業智能與數據科學

數據管理與商業智能In this heavily jargonized trade, the words typically overlap one another, leading to a scarcity of understanding or a state of confusion around these ideas. whereas big data vs analytics or computing vs machine learning vs cognitive inte…

JavaWeb網上圖書商城完整項目--day02-14.登錄功能的login頁面處理

1、現在注冊成功之后&#xff0c;我們來到登錄頁面&#xff0c;登錄頁面在于 在登錄頁面。我們也需要向注冊頁面一樣對登錄的用戶名、密碼 驗證碼等在jsp頁面中進行校驗&#xff0c;校驗我們單獨放置一個login.js文件中進行處理&#xff0c;然后login.jsp加載該js文件 我們來看…

php多線程是什么意思,多線程是什么意思

線程是操作系統能夠進行運算調度的最小單位&#xff0c;它被包含在進程之中&#xff0c;是進程中的實際運作單位&#xff0c;而多線程就是指從軟件或者硬件上實現多個線程并發執行的技術&#xff0c;具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多于一個線程&#…

c++一個類創建多個對象_C ++ | 創建一個類的多個對象

c一個類創建多個對象In the below program, we are creating a C program to create multiple objects of a class. 在下面的程序中&#xff0c;我們正在創建一個C 程序來創建一個類的多個對象 。 /* C program to create multiple objects of a class */#include <iostrea…

Activity中與ListActivity中使用listview區別

一.Activity中與ListActivity中使用listview區別&#xff08;本身沒多大區別&#xff0c;只是ListActivity在listview的顯示上做了一些優化&#xff09;Activity中使用Listview步驟&#xff1a;1.xml布局中,ListView標簽id可以任意取值如&#xff1a;<ListView andro…

java相關是什么,什么是java

java基礎常見面試題&#xff0c;這是一篇超長的隨筆&#xff01;&#xff01;&#xff01;1. Java基礎部分....................................................... 4 1、一個".java"源文件中是否可以包括多個類(不是內部類)&#xff1f;有什么限制&#xff1f;.. …

如何在Scala中將Double轉換為String?

Double in Scala is a data type that stores numerical values that have decimals. It can store a 64-bit floating point number. Scala中的Double是一種數據類型&#xff0c;用于存儲帶有小數的數值。 它可以存儲一個64位浮點數。 Example: 例&#xff1a; val decimal…

basic knowledge

Position 屬性&#xff1a;規定元素的定位類型。即元素脫離文檔流的布局&#xff0c;在頁面的任意位置顯示。 ①absolute &#xff1a;絕對定位&#xff1b;脫離文檔流的布局&#xff0c;遺留下來的空間由后面的元素填充。定位的起始位置為最近的父元素(postion不為static)&…

avatar.php uid,phpcms函數庫中獲取會員頭像方法get_memberavatar()有時無效問題

修復方法&#xff1a;首先我先給出無效情況的演示代碼&#xff0c;如下&#xff1a;$userid intval($_GET[userid]);$userinfo $this->db->get_one(userid.$userid);$this->db->set_model(10); //原因便在這里$userdetail $this->db->get_one("useri…

ruby 集合 分組_將Ruby中兩個集合的所有元素結合在一起

ruby 集合 分組In this program, we will see how we can combine the two sets? This is not a very difficult task. This can be easily done with the help of the operator. In many places of programming, you will find that operator is overloaded for various ty…

?Python中面向對象的編程

Python面向對象的編程1概述&#xff08;1&#xff09;面向對象編程面向對象的編程是利用“類”和“對象”來創建各種模型來實現對真實世界的描述&#xff0c;使用面向對象編程的原因一方面是因為它可以使程序的維護和擴展變得更簡單&#xff0c;并且可以大大提高程序開發效率&a…

php中用for循環制作矩形,PHP中for循環語句的幾種變型

PHP中for循環語句的幾種變型2021-01-22 10:21:42406for語句可以說是PHP(同時也是多種語言)的循環控制部份最基本的一個語句了&#xff0c;for語句的執行規律和基礎用法在這里就不多說&#xff0c;可以參見PHP手冊for語句部分。PHP手冊中對它的語法定義如下&#xff1a;for(expr…

c語言用命令行編譯運行程序_使用C程序執行系統命令

c語言用命令行編譯運行程序Sometimes, we may need to execute Linux/Windows DOS commands through our C program. (Note: the code given below is compiled and executed on Linux GCC compiler, so here we are testing Linux commands only). 有時&#xff0c;我們可能需…

python 熊貓,Python熊貓

我試圖連續分組和計算相同的信息&#xff1a;#Functionsdef postal_saude ():global df, lista_solic#List of solicitantes in Postal Saudelist_sol [lista_solic["name1"], lista_solic["name2"]]#filter Postal Saude Solicitantesdf df[(df[Cliente…

Spring的兩種任務調度Scheduled和Async

Spring提供了兩種后臺任務的方法,分別是: 調度任務&#xff0c;Schedule異步任務&#xff0c;Async當然&#xff0c;使用這兩個是有條件的&#xff0c;需要在spring應用的上下文中聲明<task:annotation-driven/>當然&#xff0c;如果我們是基于java配置的&#xff0c;需要…

建立單鏈表 單鏈表的插入_單鏈列表插入

建立單鏈表 單鏈表的插入All possible cases: 所有可能的情況&#xff1a; Inserting at beginning 開始插入 Inserting at the ending 在末尾插入 Inserting at given position 在給定位置插入 Algorithms: 算法&#xff1a; 1)開始插入 (1) Inserting at the beginning) In…

mysql學習筆記(1-安裝簡介)

mysql的安裝方式&#xff1a;(1)通過系統提供的默認版本(rpm包&#xff0c;穩定版&#xff0c;該版本滿足了使用的需求&#xff0c;建議使用&#xff0c;os vendor)(2)mysql官方提供官方提供的通用rpm安裝包通用二進制格式的程序包(直接下載文件&#xff0c;解壓到指定目錄&…

存儲器間接尋址方式_8086中的數據存儲器尋址模式

存儲器間接尋址方式In this type of addressing mode, first the offset address is calculated, then the memory address is calculated and then the operand form that memory location is fetched. There are following modes which lie under the Data Addressing Mode: …

oracle asm 刪除diskgroup,ASM磁盤組刪除DISK操作

沒想到這么簡單的操作&#xff0c;由于不熟悉還碰到了兩個小問題。[oracledbserver1 ~]$ sqlplus / as sysdbaSQL*Plus: Release 11.2.0.2.0 Production on Tue Aug 9 10:08:062011Copyright (c) 1982, 2010, Oracle.All rights reserved.Connected to:Oracle Database 11g Ent…