什么是NodeJS
Node.js采用模塊化結(jié)構(gòu),按照CommonJS規(guī)范定義和使用模塊。模塊與文件是一一對(duì)應(yīng)關(guān)系重慢,即加載一個(gè)模塊今阳,實(shí)際上就是加載對(duì)應(yīng)的一個(gè)模塊文件师溅。
JS是腳本語(yǔ)言茅信,腳本語(yǔ)言都需要一個(gè)解析器才能運(yùn)行。對(duì)于寫(xiě)在HTML頁(yè)面里的JS墓臭,瀏覽器充當(dāng)了解析器的角色蘸鲸。而對(duì)于需要獨(dú)立運(yùn)行的JS,NodeJS就是一個(gè)解析器窿锉。
每一種解析器都是一個(gè)運(yùn)行環(huán)境酌摇,不但允許JS定義各種數(shù)據(jù)結(jié)構(gòu),進(jìn)行各種計(jì)算嗡载,還允許JS使用運(yùn)行環(huán)境提供的內(nèi)置對(duì)象和方法做一些事情窑多。例如運(yùn)行在瀏覽器中的JS的用途是操作DOM,瀏覽器就提供了document之類(lèi)的內(nèi)置對(duì)象鼻疮。而運(yùn)行在NodeJS中的JS的用途是操作磁盤(pán)文件或搭建HTTP服務(wù)器怯伊,NodeJS就相應(yīng)提供了fs、http等內(nèi)置對(duì)象判沟。
Node.js 被設(shè)計(jì)用來(lái)開(kāi)發(fā)大規(guī)模高并發(fā)的網(wǎng)絡(luò)應(yīng)用耿芹,這種網(wǎng)絡(luò)應(yīng)用的瓶頸之一是在 I/O 的處理效率上。由于硬件及網(wǎng)絡(luò)的限制挪哄,I/O 的速度往往是固定的吧秕,如何在此前提下盡可能處理更多的客戶(hù)請(qǐng)求,提高 CPU 使用效率迹炼,便成了開(kāi)發(fā)人員面臨的最大問(wèn)題砸彬。得益于基于事件驅(qū)動(dòng)的編程模型,Node.js 使用單一的 Event loop 線程處理客戶(hù)請(qǐng)求斯入,將 I/O 操作分派至各異步處理模塊(這里一般人不理解砂碉,node.js包含很多模塊,這些模塊可以使用js直接調(diào)用系統(tǒng)的api)刻两,既解決了單線程模式下 I/O 阻塞的問(wèn)題增蹭,又避免了多線程模式下資源分配及搶占的問(wèn)題。
單線程模式
客戶(hù)端發(fā)起一個(gè) I/O 請(qǐng)求(數(shù)據(jù)庫(kù)查詢(xún))磅摹,然后等待服務(wù)器端返回 I/O 結(jié)果滋迈,結(jié)果返回后再對(duì)其進(jìn)行操作,但這種請(qǐng)求常常需要很長(zhǎng)時(shí)間(對(duì)于服務(wù)器的 CPU 處理能力來(lái)說(shuō))户誓。這一過(guò)程中饼灿,服務(wù)器無(wú)法接受新的請(qǐng)求,即阻塞式 I/O帝美。這種處理方式雖然簡(jiǎn)單碍彭,卻不實(shí)用,尤其是面對(duì)大量請(qǐng)求的時(shí)候,簡(jiǎn)直就不可用硕旗。這種情景類(lèi)似在火車(chē)站售票窗口排隊(duì)買(mǎi)票窗骑,如果您在春節(jié)期間去北京火車(chē)站排隊(duì)買(mǎi)過(guò)票,絕不會(huì)認(rèn)為這是一種好的處理方式漆枚。慶幸的是创译,現(xiàn)在很少有服務(wù)器采取這種處理方式。
多線程模式
該方式下墙基,服務(wù)器為每個(gè)請(qǐng)求分配一個(gè)線程软族,所有任務(wù)均在自己的線程內(nèi)執(zhí)行,就像火車(chē)站多開(kāi)了幾個(gè)賣(mài)票窗口残制,處理效率高了許多立砸。但就如讀者看到的那樣,在春節(jié)期間各個(gè)售票窗口前還是人滿(mǎn)為患初茶,為什么火車(chē)站不再多開(kāi)一些售票窗口呢颗祝?當(dāng)然是因?yàn)槌杀尽>€程也一樣恼布,服務(wù)器每創(chuàng)建一個(gè)線程螺戳,每個(gè)線程大概會(huì)占用 2M 的系統(tǒng)內(nèi)存,而且線程之間的切換也會(huì)降低服務(wù)器的處理效率折汞,基于成本的考慮倔幼,這種處理方式也有一定的局限性。然而爽待,這卻不是最主要的损同,主要的是開(kāi)發(fā)多線程程序非常困難,容易出錯(cuò)鸟款。程序員需考慮死鎖膏燃,數(shù)據(jù)不一致等問(wèn)題,多線程的程序極難調(diào)試和測(cè)試何什√闵遥基本上在程序運(yùn)行出錯(cuò)的時(shí)候,程序員才知道自己的程序有錯(cuò)誤富俄。而這種錯(cuò)誤的代價(jià)往往又是巨大的,那些訪問(wèn)量巨大的電子商務(wù)網(wǎng)站時(shí)常會(huì)曝出價(jià)格錯(cuò)誤等導(dǎo)致公司損失的新聞而咆。
事件驅(qū)動(dòng)
客戶(hù)發(fā)起 I/O 請(qǐng)求的同時(shí)傳入一個(gè)函數(shù)霍比,該函數(shù)會(huì)在 I/O 結(jié)果返回后被自動(dòng)調(diào)用,而且該請(qǐng)求不會(huì)阻塞后續(xù)操作暴备。就像電話訂票悠瞬,設(shè)想你一大早來(lái)到辦公室,給火車(chē)站打個(gè)電話,將自己的票務(wù)信息浅妆,地址告訴對(duì)方望迎,然后放下電話,泡杯茶凌外,瀏覽一下網(wǎng)頁(yè)辩尊,回復(fù)一下今天的電子郵件,你完全不用管火車(chē)票的事了康辑,如果訂到票摄欲,火車(chē)站會(huì)派快遞公司按你電話中提到的聯(lián)系方式送票給你。無(wú)疑疮薇,這是一種極其理想的處理方式胸墙。所有請(qǐng)求以及同時(shí)傳入的回調(diào)函數(shù)均發(fā)送至同一線程,該線程通常叫做 Event loop 線程按咒,該線程負(fù)責(zé)在 I/O 執(zhí)行完畢后迟隅,將結(jié)果返回給回調(diào)函數(shù)。這里要注意的是 I/O 操作本身并不在該線程內(nèi)執(zhí)行励七,所以不會(huì)阻塞后續(xù)請(qǐng)求智袭。比如:請(qǐng)求a要訪問(wèn)數(shù)據(jù)庫(kù),請(qǐng)求b要訪問(wèn)文件系統(tǒng)呀伙,假設(shè)Event loop先接受到a請(qǐng)求补履,這時(shí)Event loop會(huì)把a(bǔ)的回調(diào)方法交給處理訪問(wèn)數(shù)據(jù)庫(kù)的異步處理模塊。然后Event loop就可以去接受請(qǐng)求b,并把b的回調(diào)方法交給處理文件系統(tǒng)的一部處理模塊剿另。然后Event loop繼續(xù)等待請(qǐng)求箫锤。當(dāng)訪問(wèn)數(shù)據(jù)的異步處理模塊處理完成后,會(huì)主動(dòng)調(diào)用a的回調(diào)方法雨女。在a的回調(diào)方法中谚攒,就會(huì)給客戶(hù)a發(fā)送查詢(xún)到的數(shù)據(jù)(當(dāng)然這里需要短暫的使用Event loop來(lái)操作)。
為什么選用 JavaScript
事實(shí)上氛堕,在實(shí)現(xiàn) Node.js 之初馏臭,作者 Ryan Dahl 并沒(méi)有選擇 JavaScript,他嘗試過(guò) C讼稚、Lua括儒,皆因其欠缺一些高級(jí)語(yǔ)言的特性,如閉包锐想、函數(shù)式編程帮寻,致使程序復(fù)雜,難以維護(hù)赠摇。而 JavaScript 則是支持函數(shù)式編程范型的語(yǔ)言固逗,很好地契合了 Node.js 基于事件驅(qū)動(dòng)的編程模型浅蚪。加之 Google 提供的 V8 引擎,使 JavaScript 語(yǔ)言的執(zhí)行速度大大提高烫罩。最終呈現(xiàn)在我們面前的就成了 Node.js惜傲,而不是 Node.c,Node.lua 或其他語(yǔ)言的實(shí)現(xiàn)贝攒。Javascript的匿名函數(shù)和閉包特性非常適合事件驅(qū)動(dòng)盗誊、異步編程。Javascript在動(dòng)態(tài)語(yǔ)言中性能較好饿这,有開(kāi)發(fā)人員對(duì)Javacript浊伙、Python、Ruby等動(dòng)態(tài)語(yǔ)言做了性能分析长捧,發(fā)現(xiàn)Javascript的性能要好于其他語(yǔ)言嚣鄙,再加上V8引擎也是同類(lèi)的佼佼者,所以Node.js的性能也受益其中串结。
Node.js采用C++語(yǔ)言編寫(xiě)而成哑子,是一個(gè)Javascript的運(yùn)行環(huán)境。為什么采用C++語(yǔ)言呢肌割?據(jù)Node.js創(chuàng)始人Ryan Dahl回憶卧蜓,他最初希望采用Ruby來(lái)寫(xiě)Node.js,但是后來(lái)發(fā)現(xiàn)Ruby虛擬機(jī)的性能不能滿(mǎn)足他的要求把敞,后來(lái)他嘗試采用V8引擎弥奸,所以選擇了C++語(yǔ)言。
Node.js采用了Google Chrome瀏覽器的V8引擎奋早,性能很好盛霎,同時(shí)還提供了很多系統(tǒng)級(jí)的API,如文件操作耽装、網(wǎng)絡(luò)編程等愤炸。
Node.js是一個(gè)后端的Javascript運(yùn)行環(huán)境(支持的系統(tǒng)包括Linux、Windows)掉奄,這意味著你可以編寫(xiě)系統(tǒng)級(jí)或者服務(wù)器端的Javascript代碼规个,交給Node.js來(lái)解釋執(zhí)行。
多核處理器情況下
NodeJS中的JavaScript確實(shí)是在單線程上執(zhí)行姓建,但是作為宿主的NodeJS诞仓,它本身并非是單線程的,NodeJS在I/O方面又動(dòng)用到一小部分額外的線程協(xié)助實(shí)現(xiàn)異步速兔。程序員沒(méi)有機(jī)會(huì)直接創(chuàng)建線程狂芋,這也是有的同學(xué)想當(dāng)然的認(rèn)為NodeJS的單線程無(wú)法很好的利用多核CPU的原因,他們甚至?xí)f(shuō)憨栽,難以想象由多人一起協(xié)作開(kāi)發(fā)一個(gè)單線程的程序。
NodeJS封裝了內(nèi)部的異步實(shí)現(xiàn)后,導(dǎo)致程序員無(wú)法直接操作線程屑柔,也就造成所有的業(yè)務(wù)邏輯運(yùn)算都會(huì)丟到JavaScript的執(zhí)行線程上屡萤,這也就意味著,在高并發(fā)請(qǐng)求的時(shí)候掸宛,I/O的問(wèn)題是很好的解決了死陆,但是所有的業(yè)務(wù)邏輯運(yùn)算積少成多地都運(yùn)行在JavaScript線程上,形成了一條擁擠的JavaScript運(yùn)算線程唧瘾。NodeJS的弱點(diǎn)在這個(gè)時(shí)候會(huì)暴露出來(lái)措译,單線程執(zhí)行運(yùn)算形成的瓶頸,拖慢了I/O的效率饰序。這大概可以算得上是密集運(yùn)算情況下無(wú)法很好利用多核CPU的缺點(diǎn)领虹。這條擁擠的JavaScript線程,給I/O形成了性能上限求豫。
但是塌衰,事情又并非絕對(duì)的◎鸺危回到前端瀏覽器中最疆,為了解決線程擁擠的情況,Web Worker應(yīng)運(yùn)而生蚤告。而同樣努酸,Node也提供了child_process.fork來(lái)創(chuàng)建Node的子進(jìn)程。在一個(gè)Node進(jìn)程就能很好的解決密集I/O的情況下杜恰,fork出來(lái)的其余Node子進(jìn)程可以當(dāng)作常駐服務(wù)來(lái)解決運(yùn)算阻塞的問(wèn)題(將運(yùn)算分發(fā)到多個(gè)Node子進(jìn)程中上去获诈,與Apache創(chuàng)建多個(gè)子進(jìn)程類(lèi)似)。當(dāng)然child_process/Web Worker的機(jī)制永遠(yuǎn)只能解決單臺(tái)機(jī)器的問(wèn)題箫章,大的Web應(yīng)用是不可能一臺(tái)服務(wù)器就能完成所有的請(qǐng)求服務(wù)的烙荷。拜NodeJS在I/O上的優(yōu)勢(shì),跨OS的多Node之間通信的是不算什么問(wèn)題的檬寂。解決NodeJS的運(yùn)算密集問(wèn)題的答案其實(shí)也是非常簡(jiǎn)單的终抽,就是將運(yùn)算分發(fā)到多個(gè)CPU上。
模塊
編寫(xiě)稍大一點(diǎn)的程序時(shí)一般都會(huì)將代碼模塊化桶至。在NodeJS中昼伴,一般將代碼合理拆分到不同的JS文件中,每一個(gè)文件就是一個(gè)模塊镣屹,而文件路徑就是模塊名圃郊。
在編寫(xiě)每個(gè)模塊時(shí),都有require女蜈、exports持舆、module三個(gè)預(yù)先定義好的變量可供使用色瘩。
require
require函數(shù)用于在當(dāng)前模塊中加載和使用別的模塊,傳入一個(gè)模塊名逸寓,返回一個(gè)模塊導(dǎo)出對(duì)象居兆。模塊名可使用相對(duì)路徑(以./開(kāi)頭),或者是絕對(duì)路徑(以/或C:之類(lèi)的盤(pán)符開(kāi)頭)竹伸。
exports
exports對(duì)象是當(dāng)前模塊的導(dǎo)出對(duì)象泥栖,用于導(dǎo)出模塊公有方法和屬性。別的模塊通過(guò)require函數(shù)使用當(dāng)前模塊時(shí)得到的就是當(dāng)前模塊的exports對(duì)象勋篓。
module
通過(guò)module對(duì)象可以訪問(wèn)到當(dāng)前模塊的一些相關(guān)信息吧享,但最多的用途是替換當(dāng)前模塊的導(dǎo)出對(duì)象。例如模塊導(dǎo)出對(duì)象默認(rèn)是一個(gè)普通對(duì)象譬嚣,如果想改成一個(gè)函數(shù)的話钢颂,可以使用以下方式。
module.exports = function () {
console.log('Hello World!');
};
以上代碼中孤荣,模塊默認(rèn)導(dǎo)出對(duì)象被替換為一個(gè)函數(shù)甸陌。
模塊初始化
一個(gè)模塊中的JS代碼僅在模塊第一次被使用時(shí)執(zhí)行一次,并在執(zhí)行過(guò)程中初始化模塊的導(dǎo)出對(duì)象盐股。之后钱豁,緩存起來(lái)的導(dǎo)出對(duì)象被重復(fù)利用。
主模塊
通過(guò)命令行參數(shù)傳遞給NodeJS以啟動(dòng)程序的模塊被稱(chēng)為主模塊疯汁。主模塊負(fù)責(zé)調(diào)度組成整個(gè)程序的其它模塊完成工作牲尺。例如通過(guò)以下命令啟動(dòng)程序時(shí),main.js就是主模塊幌蚊。
node main.js
二進(jìn)制模塊
雖然一般我們使用JS編寫(xiě)模塊谤碳,但NodeJS也支持使用C/C++編寫(xiě)二進(jìn)制模塊。編譯好的二進(jìn)制模塊除了文件擴(kuò)展名是.node外溢豆,和JS模塊的使用方式相同蜒简。雖然二進(jìn)制模塊能使用操作系統(tǒng)提供的所有功能,擁有無(wú)限的潛能漩仙,但對(duì)于前端同學(xué)而言編寫(xiě)過(guò)于困難搓茬,并且難以跨平臺(tái)使用。