Node小結(jié)

最近工作中用到了Node,實(shí)現(xiàn)了一個(gè)數(shù)據(jù)抓取處理的自動(dòng)化工具。平時(shí)的使用中,主要還是依賴(lài)各種庫(kù)。對(duì)Node本身的一些原理性的東西也不是很清楚,只是會(huì)參考文檔使用API,所以需要學(xué)習(xí)總結(jié)一下~

要點(diǎn)

  • Node平臺(tái)的結(jié)構(gòu)
  • js調(diào)用C++
  • 異步IO
  • 事件循環(huán)
  • 異步流程處理
  • 模塊的加載和查找
  • 相關(guān)的工具

Node平臺(tái)的結(jié)構(gòu)

? Node使用js來(lái)進(jìn)行開(kāi)發(fā),既可以用來(lái)寫(xiě)一些命令行的腳本工具,也可以用來(lái)開(kāi)發(fā)服務(wù)端接口。顯然js已經(jīng)

脫離了瀏覽器的運(yùn)行環(huán)境,并且平時(shí)的開(kāi)發(fā)都是異步為主js又是單線(xiàn)程,是如何實(shí)現(xiàn)呢。需要先了解一下Node的

結(jié)構(gòu):

Node架構(gòu).png

? 在Node開(kāi)始運(yùn)行的時(shí)候,會(huì)進(jìn)行C++庫(kù)的加載。在js代碼運(yùn)行的時(shí)候,通過(guò)V8解析執(zhí)行,再由于Node

bindings綁定到C++的調(diào)用上,這一切發(fā)生在V8的運(yùn)行階段。V8保證了js的運(yùn)行環(huán)境,所以js可以在任何有v8的

環(huán)境中運(yùn)行。libuv是一個(gè)主要來(lái)負(fù)責(zé)Node中異步處理和事件驅(qū)動(dòng)以及跨平臺(tái)的調(diào)用,也就是說(shuō)在js中對(duì)于系統(tǒng)

api的調(diào)用都會(huì)通過(guò)libuv來(lái)實(shí)現(xiàn),js只負(fù)責(zé)了調(diào)用和回調(diào)的處理。js和C++部分的調(diào)用有需要通過(guò)jsBinding來(lái)

處理的。

? Node中的異步主要是通過(guò)libuv來(lái)實(shí)現(xiàn),當(dāng)任務(wù)完成之后,libuv通過(guò)回調(diào)來(lái)通知js層。

js調(diào)用C++

js調(diào)用C++主要依賴(lài)于V8的運(yùn)行時(shí),現(xiàn)在考慮以下js代碼如何最終調(diào)用libuv中的文件讀寫(xiě)函數(shù):

var fs = require('fs');
fs.readFileSync('path');

對(duì)于libuv部分的C++庫(kù),在Node加載的時(shí)候會(huì)被加載到V8中等待Js的調(diào)用。

對(duì)于JS部分的代碼,會(huì)通過(guò)V8進(jìn)行編譯解釋?zhuān)偻ㄟ^(guò)jsbinding調(diào)用到C++的接口。既然JS能夠調(diào)用到C++的接口,

那么C++中必然有相應(yīng)能夠表示JS對(duì)象和方法的對(duì)象,在Node中通過(guò)XXWrap來(lái)進(jìn)行封裝的。比如針對(duì)現(xiàn)在的例子

fs對(duì)應(yīng)的就是FSReqWrap對(duì)象,它位于node_file.cc文件中。

在Node開(kāi)始執(zhí)行js代碼之前,首先會(huì)對(duì)自己進(jìn)行初始化,加載自身的C++庫(kù)。對(duì)于C++庫(kù)中的對(duì)象,以當(dāng)前代碼

為例子它不會(huì)直接創(chuàng)建FSReqWrap對(duì)象,而是將該對(duì)象的初始化函數(shù)通過(guò)node_module_register函數(shù)保存到一個(gè)

全局的表中,那么什么時(shí)候會(huì)真正初始化對(duì)象呢。

在fs.js源文件中有:

const binding = process.binding('fs');

也就是在這行代碼被調(diào)用的時(shí)候,F(xiàn)SReqWrap對(duì)象開(kāi)始初始化,以便可以js調(diào)用

在下面代碼執(zhí)行的時(shí)候,上面的process.binding('fs')也就會(huì)被調(diào)用了:

var fs = rquire('fs');

也就是說(shuō)什么時(shí)候你使用了fs這個(gè)庫(kù),什么時(shí)候C++中對(duì)應(yīng)的FSReqWrap對(duì)象進(jìn)行加載和初始化。這也是為了加載

時(shí)間以及運(yùn)行效率考慮吧,類(lèi)似懶加載的策略。

現(xiàn)在來(lái)看一下FSReqWrap初始化的時(shí)候做了哪些事呢:

在node_file.cc中:

因?yàn)槌跏蓟瘮?shù)比較長(zhǎng),所以分成2塊:

C++對(duì)象初始化1.png

這一部分其實(shí)是往上下文對(duì)象中注冊(cè)當(dāng)前對(duì)象的函數(shù)信息,例如js中調(diào)用'read'的時(shí)候,對(duì)應(yīng)到C++對(duì)象中對(duì)應(yīng)

的函數(shù)是哪一個(gè)

C++對(duì)象初始化2.png

這一部分主要是往上下文對(duì)象context中注冊(cè)類(lèi)型信息,也就是說(shuō)在js中使用對(duì)象'fs'對(duì)應(yīng)到C++中是哪一個(gè)對(duì)象

通過(guò)這種方式,將js中某個(gè)對(duì)象的某個(gè)方法調(diào)用和C++中的實(shí)現(xiàn)對(duì)應(yīng)關(guān)系存到上線(xiàn)文對(duì)象context中了。

剩下的部分只需要V8解釋執(zhí)行js的時(shí)候,從上下文對(duì)象中獲取這些信息就能將js的調(diào)用轉(zhuǎn)化到C++的調(diào)用上了

實(shí)質(zhì)上在V8解釋js的時(shí)候主要運(yùn)用了兩個(gè)函數(shù):

script::Compile
script::run

這兩個(gè)函數(shù)調(diào)用的時(shí)候,V8會(huì)將上下文對(duì)象context傳入。

現(xiàn)在通過(guò)一張圖總結(jié)一下整個(gè)過(guò)程:圖中context有標(biāo)號(hào)1和2,代表著它們的執(zhí)行順序。

js調(diào)用C++.png

右邊的部分會(huì)在Node初始化的時(shí)候進(jìn)行加載,這個(gè)過(guò)程主要是initFs初始化函數(shù)指針,F(xiàn)SReqWrap類(lèi)型信息以及FSReqWrap類(lèi)型包含的函數(shù)注冊(cè)到全局表中。在之后js代碼調(diào)用中再根據(jù)需要去完成FSReqWrap的初始化,并通過(guò)上下文對(duì)象查找到需要調(diào)用的FSReqWrap和相關(guān)函數(shù)。

異步IO

還是通過(guò)文件讀寫(xiě)的例子來(lái)進(jìn)行說(shuō)明:

var fs = require('fs');
fs.readFile('path1', 'utf-8', callback1);
fs.readFile('path2', 'utf-8', callback2);
fs.readFile('path3', 'utf-8', callback3);

現(xiàn)在通過(guò)fs對(duì)象讀取文本文件,都是異步的方式,傳入回調(diào)函數(shù)。在JS層是調(diào)用readFile之后直接返回接著執(zhí)行后面的代碼,當(dāng)文件讀取完成在執(zhí)行相應(yīng)的callback。文件的讀寫(xiě)功能在Node中通過(guò)libuv來(lái)實(shí)現(xiàn),在libuv中文件讀寫(xiě)內(nèi)部是通過(guò)不同的線(xiàn)程來(lái)完成,內(nèi)部維護(hù)這一個(gè)線(xiàn)程池。

如下圖所示:

異步IO.png

左側(cè)綠色部分的流程是js所在線(xiàn)程的執(zhí)行過(guò)程,每一次調(diào)用readFile之后接著往下執(zhí)行下一個(gè)readFile。對(duì)于每一次readFile的調(diào)用最終通過(guò)NodeBindings轉(zhuǎn)換到libuv庫(kù)中相關(guān)函數(shù)的調(diào)用,每一次文件讀取派發(fā)到一個(gè)線(xiàn)程來(lái)完成。等待文件讀取完成,再通知js層callback回調(diào)。通過(guò)分析js層的調(diào)用順序應(yīng)該是fun1->fun2->fun3 后面3個(gè)callback的先后順序則是依賴(lài)libuv中文件讀取完成的先后順序,通過(guò)libuv來(lái)實(shí)現(xiàn)了js層的異步IO。

事件循環(huán)

在異步IO小結(jié)中,說(shuō)明了Node中的異步IO是如何設(shè)計(jì)的。針對(duì)具體libuv如何通知js回調(diào)的過(guò)程,就需要了解一下

Node中事件循環(huán)中的設(shè)計(jì):

事件循環(huán)隊(duì)列.png

libuv將各種系統(tǒng)層面的操作都封裝成了一個(gè)事件對(duì)象,js和libuv之間的交互都是以事件進(jìn)行驅(qū)動(dòng)的。圖中左側(cè)是libuv中設(shè)計(jì)的用于存儲(chǔ)各種事件的隊(duì)列,右側(cè)則是uv_run函數(shù)運(yùn)行的基本流程,類(lèi)似一個(gè)不斷迭代的循環(huán),每次迭代的過(guò)程中去檢查各類(lèi)對(duì)列中有沒(méi)有需要處理的已經(jīng)完成的事件,來(lái)驅(qū)動(dòng)后面的流程。

通過(guò)文件讀寫(xiě)的例子來(lái)說(shuō)明一下詳細(xì)過(guò)程:

事件循環(huán)過(guò)程.png

上圖中淺藍(lán)色的部分是整個(gè)流程中與文件讀寫(xiě)有關(guān)系的部分,當(dāng)js層調(diào)用了文件讀取的函數(shù)后,通過(guò)jsbinding轉(zhuǎn)化成對(duì)libuv層的調(diào)用。libuv會(huì)先將文件讀寫(xiě)的請(qǐng)求封裝成一個(gè)事件對(duì)象(存儲(chǔ)了回調(diào)函數(shù)),并將該事件對(duì)象注冊(cè)到uv_fs_events隊(duì)列中,然后將文件讀寫(xiě)的任務(wù)派發(fā)給工作線(xiàn)程池中的一個(gè)閑置線(xiàn)程。等待該線(xiàn)程文件讀寫(xiě)的任務(wù)完成之后,通過(guò)修改uv_fs_events中事件對(duì)象的狀態(tài)來(lái)進(jìn)行通知。隨后在uv_run的迭代中,檢查uv_fs_events中事件對(duì)象的狀態(tài)發(fā)生變化,然后通過(guò)回調(diào)的方式通知js層文件讀寫(xiě)已經(jīng)完成。

模塊的加載和查找

模塊分類(lèi)

在說(shuō)明Node中模塊的加載和查找之前,先看一下Node中模塊的分類(lèi):

事件循環(huán)過(guò)程.png

Node中模塊可以分為兩大類(lèi):

第一類(lèi)是原生模塊,Node自帶的功能組件,比如'http' 'fs'等。第二類(lèi)是自定義模塊,我們可以通過(guò)自己封裝一些功能到j(luò)s,C++,json文件中,C++主要是用來(lái)針對(duì)一些系統(tǒng)或者是需要性能的部分來(lái)進(jìn)行擴(kuò)展,js主要是自己定義的業(yè)務(wù)模塊,json文件可以用來(lái)放置一些數(shù)據(jù)或者配置信息。而package文件夾,最熟悉的就是通過(guò)npm install 安裝的第三方模塊了,這些都可以通過(guò)require的方式進(jìn)行加載。通常需要引用一個(gè)模塊的時(shí)候使用require()方法,作為一個(gè)模塊要導(dǎo)出你想公開(kāi)的接口使用exports/module.exports。

模塊文件加載

module加載有一套自身的流程,在此之前先看一下對(duì)于一個(gè)全新Module,Node如何加載:

module加載.png

在Node中,每個(gè)模塊都是通過(guò)一個(gè)Module對(duì)象處理的,所以當(dāng)你使用require('module')引用一個(gè)新模塊的時(shí)候,Node會(huì)創(chuàng)建一個(gè)Module對(duì)象。Module對(duì)象在初始化的過(guò)程中會(huì)初始化當(dāng)前module的exports屬性(也就是你在js文件中使用的Module.exports了),生成新的Module中使用的require函數(shù)。然后調(diào)用load方法來(lái)加載文件,它首先會(huì)進(jìn)行文件類(lèi)型的判斷,根據(jù)文件類(lèi)型進(jìn)行加載。對(duì)于C++模塊,會(huì)調(diào)用process.dlopen方法加載。對(duì)于json文件則讀取出來(lái),再通過(guò)JSON.parse轉(zhuǎn)化成json對(duì)象,并賦值給module.exports。對(duì)于js文件,會(huì)調(diào)用_compile方法,更復(fù)雜一些下面會(huì)單獨(dú)介紹。總結(jié)一下,在生成module對(duì)象的時(shí)候會(huì)初始化exports屬性和require函數(shù),然后load方法主要是加載具體的模塊文件,最終的目的是給module.exports賦值,到最后require函數(shù)返回的是新生成的module.exports屬性。

現(xiàn)在來(lái)看一下對(duì)于js文件,Node如何通過(guò)Module對(duì)象加載:

_compile函數(shù)的實(shí)現(xiàn).png

在Module的load方法中,對(duì)于js文件會(huì)調(diào)用_compile方法,在 _compile中會(huì)先調(diào)用wrap方法。wrap方法主要是對(duì)于當(dāng)前要加載js文件通過(guò)function做了一層包裝,function的參數(shù)傳入了當(dāng)前module中的exports,require以及module自身。然后將該function傳遞給v8的runInThisContext函數(shù),runInThisContext函數(shù)主要做的事情就是去執(zhí)行這個(gè)傳入的function,其實(shí)就相當(dāng)于在執(zhí)行你自己寫(xiě)好的js文件中的所有代碼,這個(gè)時(shí)候最終會(huì)執(zhí)行你自己寫(xiě)的module.exports=xxxx。也就是說(shuō)你寫(xiě)的js代碼會(huì)在require的時(shí)候被執(zhí)行,js代碼中本身使用到的require和module.exports都是通過(guò)當(dāng)時(shí)運(yùn)行環(huán)境的上下文參數(shù)傳過(guò)來(lái)的。所以和json文件加載不同的地方時(shí),module.exports屬性的賦值一個(gè)是在Node的load函數(shù)中,js文件的是在你自己的代碼中。

最后看一下對(duì)于package文件夾(以u(píng)nderscore這個(gè)庫(kù)為例),如何加載:

package加載.png

Node會(huì)找到package.json文件,讀取里面的main字段。按照main字段配置的值,去按相應(yīng)的方式(js/json/c++)加載對(duì)應(yīng)的文件。如果沒(méi)有找到package.json文件,那么就直接找對(duì)應(yīng)的index.js/index.json/index.node文件,再按照文件的方式加載。

exports和module.exports

看了一下Node v0.1版本的相關(guān)源碼,在module類(lèi)型中對(duì)于exports的定義如下:

global.exports = this.exports

所以exports相當(dāng)于一個(gè)module.exports的引用,如果是你去改版exports,對(duì)于module.exports還是之前的值并沒(méi)有影響到module.exports。

模塊文件的查找

以下是當(dāng)調(diào)用require('module')的時(shí)候,模塊文件的查找流程:

模塊文件查找流程.png

整個(gè)流程中有查找文件模塊的過(guò)程,這個(gè)后面具體細(xì)說(shuō),除此之外的流程并不復(fù)雜。可以看出在查找模塊文件的時(shí)候,文件緩存的優(yōu)先級(jí)是最高的,其次是原生模塊,最后才去文件模塊中查找。猜測(cè)是跟時(shí)間復(fù)雜度有關(guān)系,從文件緩存中應(yīng)該是最快的。其次是原生模塊,因?yàn)樵K所在的路徑位置是相對(duì)固定的。最后才是文件模塊,它的位置并不固定,查找起來(lái)就相對(duì)麻煩花費(fèi)的時(shí)間更多一些。

下面再單獨(dú)針對(duì)文件模塊的查找講一下它的過(guò)程:

require('./test')

module對(duì)象中有一個(gè)path方法,它返回的是在進(jìn)行文件查找的時(shí)候,遍歷的文件路徑:

/home/jim/repos/node/node_modules
/home/jim/repos/node_modules
/home/jim/node_modules
/home/node_modules
/node_modules

首先會(huì)在執(zhí)行腳本所在路徑以js/json/node或者文件件夾(package)的方式夾加載test,如果沒(méi)有找到就去module的path返回的路徑數(shù)組逐一查找。如果最終沒(méi)有找到就拋出異常:

文件模塊查找.png

相關(guān)工具推薦

NVM Node的安裝/卸載以及版本管理工具

NPM 模塊的安裝/卸載管理工具

NRM 模塊源的管理工具

最后是IDE:我使用的是sublime + Node插件 + sublime-text2-buildview

優(yōu)點(diǎn):編碼和測(cè)試運(yùn)行很方便,很輕量級(jí),另外log信息是以一個(gè)獨(dú)立窗口的形式呈現(xiàn)(而不是在最下面,這樣log比較多的時(shí)候就不方便查看了,主要是sublime-text2-buildview的功勞),通過(guò)安裝Node插件,在調(diào)試的時(shí)候也不用在編輯器和控制臺(tái)來(lái)回切換了,提高了效率。

Reference:

https://www.cnblogs.com/lijiayi/p/js_node_module.html

http://www.infoq.com/cn/articles/nodejs-module-mechanism/#

http://taobaofed.org/blog/2015/10/29/deep-into-node-1/

https://i5ting.github.io/wechat-dev-with-nodejs/index.html

https://luzeshu.com/tech

http://zihua.li/2012/03/use-module-exports-or-exports-in-node/

https://github.com/nodejs/node

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容