Node核心功能是事件循環(huán)朵逝,此概念也多用于JS底層行為以及許多交互系統(tǒng)中腐螟。在許多語言中栏豺,事件模型是在外層的彭沼,但JS事件一直是其語言的核心模塊缔逛,這是因為JS在很多場景下都需要處理與用戶交互的事件。在服務(wù)端沒有網(wǎng)頁DOM對應的那些有限的用戶驅(qū)動型交互事件姓惑,而是在服務(wù)器程序上對應發(fā)生的各種不同的事件,例如HTTP服務(wù)器模塊在用戶發(fā)送請求給web服務(wù)器時會觸發(fā)request事件按脚。
JS利用事件循環(huán)來合理地處理系統(tǒng)各部分的請求于毙。Node采用的方式是,所有I/O事件都應該是非阻塞的辅搬。這意味著需要讓程序暫停操作的HTTP請求唯沮、數(shù)據(jù)庫查詢脖旱、文件讀寫,以及其他事情介蛉,在數(shù)據(jù)返回之前并不暫停執(zhí)行萌庆。這些事件都將獨立運行,然后在數(shù)據(jù)準備好后觸發(fā)一個事件币旧。也就是說Node編程會用到很多回調(diào)函數(shù)践险,來處理各種I/O〈盗猓回調(diào)函數(shù)往往以級聯(lián)的方式嵌入在其他回調(diào)函數(shù)中巍虫,這與瀏覽器編程有所不同。除了用順序的方式設(shè)置好啟動項外鳍刷,大部分代碼都在處理回調(diào)函數(shù)占遥。
針對這種少見的編程風格,我們需要尋找合適服務(wù)器編程的處理模式输瓜。先從事件模型開始瓦胎,大部分人直覺上是理解事件驅(qū)動編程的,因為這和日常生活息息相關(guān)尤揣。
事件驅(qū)動的人們
假設(shè)你在燒飯搔啊,正在切青椒的時候鍋里面的東西開始沸溢出來了,你會暫停切菜把爐火關(guān)小芹缔。你不會在切青椒的同時把爐火關(guān)小坯癣,而是會采用更加安全的方式,通過快速切換工作對象來達到同樣的目的最欠。事件驅(qū)動編程也是同樣的道理示罗。通過讓程序員一次只能為一個回調(diào)函數(shù)編寫處理代碼,可以讓代碼可讀性更強芝硬,而且能夠快速地處理多個任務(wù)蚜点。
日常生活中,人們習慣于用各種內(nèi)部回調(diào)的方式來處理遇到的事件拌阴。和JS類似绍绘,我們一次只能處理一件事情。這也和JS很像迟赃,能讓事件來驅(qū)動操作很棒陪拘,但它只能以單線程的方式運行,即同一時間只能處理一件事情纤壁。
單線程的概念非常重要左刽,常有人批評Node缺乏并發(fā),也就是沒有利用機器上所有CPU來運行JS酌媒。但是同時在多個CPU上運行程序也有它的問題欠痴,即需要協(xié)調(diào)多個執(zhí)行線程迄靠。要讓多個CPU有效地拆分任務(wù),它們之間需要不停地交換信息喇辽,比如當前執(zhí)行狀態(tài)以及各自完成了那些工作掌挚。雖然這不是不可能,但這么復雜的模型給程序員和系統(tǒng)帶來了很大的工作量菩咨。JS的方式很簡單吠式,同一時刻只有一件事情在操作。Node做的每一件事情都是非阻塞的旦委,所以事件觸發(fā)與Node對其操作的時間間隔是很短的奇徒,因為Node不需要等待諸如磁盤I/O這樣的操作。
事件驅(qū)動的快遞員
以快遞員投遞為例幫助你理解事件循環(huán)缨硝∧Ω疲快遞員的每個快件都是一個事件,他有一堆快件等著要按順序處理查辩,每個快件都要走相應的路徑進行投遞胖笛。路徑就是對此事件的回調(diào)函數(shù)∫说海可憐的是长踊,快遞員只有一雙腿,每次只能走其中一條路徑萍倡。
偶爾身弊,當快遞員在路上行走時,有人會給它派發(fā)另一快件列敲,這就像是投遞途中的回調(diào)函數(shù)阱佛。這種情況下,快遞員會馬上去派送新的快件戴而。此時快遞員會立即切換到新的路徑去投遞凑术。完成后在回到之前的路徑上繼續(xù)工作。
對比快遞員的行為和一般程序的做法所意。假設(shè)web服務(wù)器被請求要從數(shù)據(jù)庫讀取數(shù)據(jù)淮逊,然后返回給用戶。這種情況下扶踊,我們只要處理很少的事件泄鹏。首先,用戶的請求是要web服務(wù)器返回一個網(wǎng)頁秧耗。處理此個初始請求的回調(diào)函數(shù)A會先從請求的對象中確定它要從數(shù)據(jù)庫讀取什么內(nèi)容命满,然后向數(shù)據(jù)庫發(fā)起具體的請求,并傳入一個回調(diào)函數(shù)B供請求完成時使用绣版。處理完請求后胶台,回調(diào)函數(shù)A結(jié)束并返回。當數(shù)據(jù)庫找到需要的內(nèi)容杂抽,再觸發(fā)相應事件诈唬。事件循環(huán)隊列則調(diào)用回調(diào)函數(shù)B,讓它把數(shù)據(jù)發(fā)送給用戶缩麸。
這似乎非常直觀铸磅,這里需要特別注意的是代碼隔斷的地方,這也是過程式編程不會遇到的情況杭朱。因為Node是一個非阻塞的系統(tǒng)阅仔,所以當調(diào)用需要阻塞等待的數(shù)據(jù)庫函數(shù)時,我們會采用回調(diào)函數(shù)替代閑置等待弧械。這就是說八酒,由另外一些函數(shù)來接管這個請求,并在數(shù)據(jù)準備好返回時把它處理掉刃唐。所以我們需要確定回調(diào)函數(shù)所要用到的數(shù)據(jù)能夠有辦法取得羞迷。JS編程通常是利用閉包來實現(xiàn)這個功能的。
事件驅(qū)動的快餐店
為什么Node更加高效呢画饥?想象下在快餐店點餐衔瓮,你站在柜臺前排隊時,服務(wù)員有兩種方式來處理你的點單抖甘,一種是事件驅(qū)動的热鞍,另一種則不是。先采用PHP等許多web平臺所使用的方式衔彻。你點餐時服務(wù)員先招待你薇宠,待你點完后才服務(wù)下一個客人。他輸入完你的單子后可以做以下幾件事情:收款米奸、為你倒飲料等昼接。但是,服務(wù)員還不知道要等多久廚房才能把你的快餐做好悴晰。在傳統(tǒng)web服務(wù)框架下慢睡,每個服務(wù)程序(線程 )每次只能服務(wù)一個請求。唯一增加處理能力的方法是加入更多的線程铡溪。很顯然這樣的做法并不是那么的高效漂辐,服務(wù)員在等待廚房做菜時浪費了很多時間。
顯然現(xiàn)實生活的餐館使用的是更加高效的模式棕硫。你點完菜后髓涯,服務(wù)員會給你一個號碼,在菜做好時通知你哈扮,你可以稱之為回調(diào)號碼纬纪。Node也是這樣工作的蚓再。當I/O一類費時操作開始時,Node會給它們一個回調(diào)引用包各,然后繼續(xù)處理其他已經(jīng)就緒的工作摘仅。比如服務(wù)員可服務(wù)下一個客人,對Node來說則是下一個事件问畅。需要重點關(guān)注的是娃属。當呼叫某位客人來取食物的時候,他們不會處理新客人的需求护姆,反之亦然矾端。通過事件驅(qū)動的運作方式,服務(wù)員能夠最大程度地提高產(chǎn)出卵皂。
在一些小餐館秩铆,廚師和服務(wù)員是同一個人,這種情況下采用事件驅(qū)動并不能提高效率渐裂,因為所有的工作都由同一個人完成豺旬,事件驅(qū)動的架構(gòu)并不能增加價值。如果服務(wù)器的全部或大部分工作是進行運算柒凉,Node并非最理想的模型族阅。
假設(shè)餐館中有兩名服務(wù)員和四位客人。如果服務(wù)員一次只能服務(wù)一位客人膝捞,那么頭兩個客人可最快地拿到食物坦刀,而剩余兩位的體驗會很糟糕。前兩位客人之所以能夠快速地獲得食物是因為服務(wù)員在全力滿足他們的需求蔬咬,這占用了后兩位客人的時間鲤遥。在事件驅(qū)動模型下,頭兩位客人可能需稍微等待一下才能拿到食物林艘,因為服務(wù)員需先處理一下后兩位客人的點單盖奈,但系統(tǒng)的平均等待時間(延遲)將大大降低。
被阻塞的郵遞員
我們給事件循環(huán)模式的郵遞員一封信去投遞狐援,但投遞這封信需要經(jīng)過一扇門钢坦。郵遞員達到目的地,而門卻關(guān)閉著啥酱,所以他只能等待并不停地嘗試進入爹凹。他等待門打開就像進入了死循環(huán)模式。如果在信封隊列里有另外一封信能夠通知某人來打開門镶殷,讓郵遞員進去禾酱,這不就解決問題了嗎?不幸的是,郵遞員正在無休止地等待打開門颤陶,無法抽身去投遞那封信颗管,這是因為打開門的事件在當前回調(diào)事件的外部。如果在回調(diào)函數(shù)內(nèi)部發(fā)起事件指郁,我們知道郵遞員會優(yōu)先把這封信給投遞掉哦忙上,但是當事件是在當前執(zhí)行代碼的外部發(fā)生時,它必須等待正在執(zhí)行的代碼完成之后才會被調(diào)用闲坎。
雖然我們不太會編寫依賴外部條件作為跳出判斷的循環(huán)體,但這展示了Node同時只能處理一件事情的本質(zhì)茬斧,任何一點缺陷都可能導致整個系統(tǒng)混亂腰懂。這也是事件驅(qū)動編程的核心模塊是非阻塞I/O的原因。
var evt = require('events');
var EE = evt.EventEmitter;
var ee = new EE();
var die = false;
ee.on('die', function(){
die = true;
});
setTimeout(function(){
ee.emit('die');
}, 100);
while(!die){}
// console.log()永遠不會被調(diào)用项秉,因為while循環(huán)不會讓Node有機會觸發(fā)timeout回調(diào)函數(shù)并發(fā)起die事件绣溜。
console.log('done');
創(chuàng)建HTTP服務(wù)器
var http = require('http');
http.createServer(function(req, res){
res.writeHead(200, {Content-Type:'text/plain'});
res.end('Hi\n');
}).listen(8124, '127.0.0.1');
console.log('server running');
通過調(diào)用http庫的工廠方法來創(chuàng)建HTTP服務(wù)器,工廠方法在創(chuàng)建新的HTTP服務(wù)器的同時娄蔼,為request事件綁定了一個回調(diào)函數(shù)怖喻,后者作為createServer()的第一個參數(shù)傳遞進去。當代碼運行的時候會發(fā)生什么有趣的事情呢岁诉?
Node.js運行的第一件事情是把代碼從頭到尾運行一遍锚沸,這可以認為是Node編程的設(shè)置階段。因為我們綁定了一些事件監(jiān)聽器涕癣,所以Node.js不會退出哗蜈,而是等待這些事件被觸發(fā)。若沒有綁定任何事件坠韩,Node.js在運行完代碼后就會立即退出距潘。
當服務(wù)器接收到一個HTTP請求時會進行怎樣的處理呢?Node.js會發(fā)起request事件只搁,因為該事件有對應的回調(diào)函數(shù)綁定在上面音比,回調(diào)函數(shù)會被依次調(diào)用。
假設(shè)網(wǎng)站變得非常受歡迎氢惋,同時又很多請求進來洞翩。假設(shè)回調(diào)函數(shù)需執(zhí)行1秒。在第一個請求后緊跟著又來了第二個請求明肮,那么第二個請求將不會在這1秒內(nèi)被處理菱农。顯然1秒其實是很長的時間了。
讓我們來看看真實應用情景柿估,事件循環(huán)阻塞的問題會嚴重的破壞用戶體驗循未。HTTP服務(wù)器實際上是由操作系統(tǒng)內(nèi)核處理與客戶端的TCP連接的。所以盡管不會惡化到拒絕新連接的境地,但仍然會有這些鏈接不被處理的危險的妖。為了處理這些問題绣檬,希望盡量保持Node.js的事件驅(qū)動和非阻塞的特性。同樣的方式嫂粟,讓費時的I/O事件回調(diào)的方法來通知Node.js娇未,只有數(shù)據(jù)已經(jīng)準備好了,才可以進行下一步操作星虹。Node.js程序本身需要把每個回調(diào)函數(shù)都寫得運行迅速零抬,防止把事件循環(huán)給阻塞住。
這意味著編寫Node.js服務(wù)器程序的時候需遵循以下兩個策略:
- 在設(shè)置完成以后所有的操作都是事件驅(qū)動的
- 如果Node.js需長時間處理數(shù)據(jù)就要考慮把它分配給web worker去處理