Node.js中的事件驅(qū)動(dòng)編程詳解
在傳統(tǒng)程編程模里,I/O操作就像一個(gè)普通的本地函數(shù)調(diào)用:在函數(shù)執(zhí)行完之前程序被堵塞,無法繼續(xù)運(yùn)行。堵塞I/O起源于早先的時(shí)間片模型,這種模型下每個(gè)進(jìn)程就像一個(gè)獨(dú)立的人,目的是將每個(gè)人區(qū)分開,而且每個(gè)人在同一時(shí)刻通常只能做一件事,必須等待前面的事做完才能決定下一件事做什么。但是這種在計(jì)算機(jī)網(wǎng)絡(luò)和Internet上被廣泛使用的“一個(gè)用戶,一個(gè)進(jìn)程”的模型伸縮性很差。管理多個(gè)進(jìn)程時(shí),會耗費(fèi)很多內(nèi)存,上下文切換也會占用大量資源,這些對操作系統(tǒng)是個(gè)很大的負(fù)擔(dān),而且隨著進(jìn)程數(shù)的遞增,會導(dǎo)致系統(tǒng)性能急劇衰減。
多線程是個(gè)替代方案,線程是一個(gè)輕量級的進(jìn)程,它會和同一個(gè)進(jìn)程內(nèi)的其它線程共享內(nèi)存,它更像傳統(tǒng)模型的擴(kuò)展,用來并發(fā)執(zhí)行多個(gè)線程,當(dāng)一個(gè)線程等待I/O操作時(shí),其它線程可以接管CPU,當(dāng)I/O操作完成,前面等待的線程會被喚醒。就是說,一個(gè)運(yùn)行中的線程可以被中斷,然后稍候再被恢復(fù)。此外,在一些系統(tǒng)下線程可以在多核CPU的不同核心下并行運(yùn)行。
程序員并不知道線程會在什么具體時(shí)間運(yùn)行,他們必須很小心的處理共享內(nèi)存的并發(fā)訪問,因此必須使用一些同步原語來同步訪問某個(gè)數(shù)據(jù)結(jié)構(gòu),比如使用鎖或信號量,以此來強(qiáng)制線程以特定的行為和計(jì)劃執(zhí)行。那些大量依賴線程間的共享狀態(tài)的應(yīng)用程序,很容易就會出現(xiàn)一些隨機(jī)性很強(qiáng),難以查找的奇怪問題。
還有一種方式是使用多線程協(xié)作,由你自己負(fù)責(zé)顯式的釋放CPU,并把CPU時(shí)間交給其他線程使用,因?yàn)橛赡阌H自來控制線程的執(zhí)行計(jì)劃,因此減小了對同步的需求,但是也提高了程序的復(fù)雜度和出錯(cuò)的機(jī)會,而且并沒有避免多線程的那些問題。
什么是事件驅(qū)動(dòng)編程
事件驅(qū)動(dòng)編程(Evnet-driven programming)是一種編程風(fēng)格,由事件來決定程序的執(zhí)行流程,事件由事件處理器(event handler)或事件回調(diào)(event callback)來處理,事件回調(diào)是當(dāng)某個(gè)特定事件發(fā)生時(shí)被調(diào)用的函數(shù),比如數(shù)據(jù)庫返回了查詢結(jié)果或者用戶單擊了一個(gè)按鈕。
回想下,在傳統(tǒng)的堵塞I/O編程模式里,數(shù)據(jù)庫查詢可能像這樣:
result = query('SELECT * FROM posts WHERE id = 1');
do_something_with(result);
上面的query函數(shù)會讓當(dāng)前線程或進(jìn)程一直處于等待狀態(tài),直到底層數(shù)據(jù)庫完成查詢操作并返回。
在事件驅(qū)動(dòng)模型里,這個(gè)查詢會變成這樣:
query_finished = function(result) {
do_something_with(result);
}
query('SELECT * FROM posts WHERE id = 1', query_finished);
首先你定義了一個(gè)叫query_finished的函數(shù),它包含了查詢完成后要做的事。然后把這個(gè)函數(shù)當(dāng)做參數(shù)傳遞給query函數(shù),當(dāng)query執(zhí)行完畢會調(diào)用query_finished,而不是僅僅返回查詢結(jié)果。
當(dāng)你感興趣的事件發(fā)生時(shí)會調(diào)用你定義的函數(shù),而不是簡單的返回結(jié)果值,這種編程模型就叫事件驅(qū)動(dòng)編程或異步編程。這是Node一個(gè)最明顯的特性,這種編程模型意味著當(dāng)前進(jìn)程在執(zhí)行I/O操作時(shí)不會被阻塞,因此,多個(gè)I/O操作可以并行執(zhí)行,當(dāng)操作完成后相應(yīng)的回調(diào)函數(shù)就會被調(diào)用。
事件驅(qū)動(dòng)編程底層依賴于事件循環(huán)(event loop),事件循環(huán)基本上是事件檢測和事件處理器觸發(fā)這兩種函數(shù)不斷循環(huán)調(diào)用的一個(gè)結(jié)構(gòu)。在每次循環(huán)里,事件循環(huán)機(jī)制需要檢測發(fā)生了哪些事件,當(dāng)事件發(fā)生時(shí),它找到對應(yīng)的回調(diào)函數(shù)并調(diào)用它。
事件循環(huán)只是運(yùn)行在進(jìn)程內(nèi)的一個(gè)線程,當(dāng)事件發(fā)生時(shí),事件處理器可以單獨(dú)運(yùn)行并且不會被中斷,也就是說:
1.在某個(gè)特定時(shí)刻最多有一個(gè)事件回調(diào)函數(shù)運(yùn)行
2.任何事件處理器運(yùn)行時(shí)都不會被中斷
有了這個(gè),開發(fā)人員就可以不再為線程同步和并發(fā)修改共享內(nèi)存這些事頭疼了。
一個(gè)眾所周知的秘密:
很久以前,系統(tǒng)編程社區(qū)的人們就知道事件驅(qū)動(dòng)編程是創(chuàng)建高并發(fā)服務(wù)最佳方式,因?yàn)樗挥帽4婧芏嗌舷挛?,因此?jié)省了大量內(nèi)存,也沒有那么多上下文切換,又節(jié)省了大量執(zhí)行時(shí)間。
慢慢的,這種理念滲透到了其他的平臺和社區(qū),出現(xiàn)了一些有名的事件循環(huán)實(shí)現(xiàn),比如Ruby的Event machine,Perl的AnyEvnet,以及Python的Twisted,除了這些還有很多其它的實(shí)現(xiàn)和語言。
用這些框架做開發(fā),需要學(xué)習(xí)框架相關(guān)的特定知識以及框架特定的類庫,比如,使用Event Machine時(shí),為了享受非阻塞帶來的好處,你得避免使用同步類庫,只能用Event Machine的異步類庫。如果你使用了任何阻塞類庫(比如Ruby的大多數(shù)標(biāo)準(zhǔn)庫),你的服務(wù)器就失去了最佳的伸縮性,因?yàn)槭录h(huán)依然會不斷地被阻塞,時(shí)不時(shí)地阻礙了I/O事件的處理。
Node最初就被設(shè)計(jì)成一個(gè)非阻塞I/O服務(wù)器平臺,因此一般情況下,你應(yīng)該期望運(yùn)行在它上面的所有代碼都是非阻塞的。因?yàn)镴avaScript非常小,而且它不強(qiáng)制使用任何I/O模型(因?yàn)樗鼪]有標(biāo)準(zhǔn)的I/O類庫),因此Node建立在一個(gè)很純凈的環(huán)境里,不會有什么歷史遺留問題。
Node和JavaScript如何簡化了異步應(yīng)用程序
Node的作者Ryan Dahl,最初使用C來開發(fā)這個(gè)項(xiàng)目,但是發(fā)現(xiàn)維護(hù)函數(shù)調(diào)用的上下文太復(fù)雜,導(dǎo)致代碼復(fù)雜度很高。然后他轉(zhuǎn)用Lua,但是Lua已經(jīng)有個(gè)幾個(gè)阻塞的I/O類庫,阻塞和非阻塞混在一起可能會讓開發(fā)人員很迷惑并因此阻礙了很多人構(gòu)建可伸縮的應(yīng)用,于是Lua也被Dahl拋棄了。最后他轉(zhuǎn)向了JavaScript,JavaScript中的閉包及第一級對象的函數(shù),這些特性使JavaScript非常適合用作事件驅(qū)動(dòng)編程。JavaScript的魔力是讓Node如此流行的一個(gè)主要原因。
什么是閉包
閉包可以理解為一個(gè)特殊的函數(shù),但是它可以繼承并訪問它自身被定義的那個(gè)作用域里的變量。當(dāng)你將一個(gè)回調(diào)函數(shù)作為參數(shù)傳遞給另外一個(gè)函數(shù)時(shí),它稍候會被調(diào)用,神奇的是,這個(gè)回調(diào)函數(shù)被稍候調(diào)用時(shí),它居然記住了它自身定義所在的那個(gè)上下文以及父上下文里的變量,而且還可以正常訪問它們。這個(gè)強(qiáng)大的特性是Node成功的核心。
下面的例子將展示在Web瀏覽器里JavaScript閉包是如何工作的。假如,你要監(jiān)聽一個(gè)按鈕的單機(jī)事件,你可以這樣做:
var clickCount = 0;
document.getElementById('myButton').onclick = function() {
clickCount += 1;
alert("clicked " + clickCount + " times.");
};
使用jQuery時(shí)是這樣:
var clickCount = 0;
$('button#mybutton').click(function() {
clickedCount ++;
alert('Clicked ' + clickCount + ' times.');
});
JavaScript里,函數(shù)是第一類對象,就是說你可以把函數(shù)當(dāng)作參數(shù)來傳遞給其他函數(shù)。上面的兩個(gè)例子,前者把一個(gè)函數(shù)賦值給另一個(gè)函數(shù),后者把函數(shù)作為參數(shù)傳遞給另一個(gè)函數(shù),單擊事件的處理函數(shù)(回調(diào)函數(shù))可以訪問函數(shù)定義所在代碼塊下的每個(gè)變量,在這個(gè)例子里,它可以訪問在它父閉包內(nèi)定義的clickCount變量。
clickCount變量處在全局作用域(JavaScript里最外層的作用域),它保存了用戶點(diǎn)擊按鈕的次數(shù),通常在全局作用域下存儲變量是個(gè)壞習(xí)慣,因?yàn)槟菢雍苋菀赘渌a沖突,你應(yīng)該把變量放在使用它們的本地作用域里。大多時(shí)候,只用把代碼用一個(gè)函數(shù)包裝起來,等于另外創(chuàng)建了閉包,這樣就可以很容易避免污染全局環(huán)境,就像這樣:
(function() {
var clickCount = 0;
$('button#mybutton').click(function() {
clickCount ++;
alert('Clicked ' + clickCount + ' times.');
});
}());
注意:上面代碼的第七行,定義了一個(gè)函數(shù)后立刻調(diào)用它,這是JavaScript里一個(gè)常見的設(shè)計(jì)模式:通過創(chuàng)建函數(shù)來創(chuàng)建一個(gè)新的作用域。
閉包如何幫助異步編程
在事件驅(qū)動(dòng)編程模型里,先編寫事件發(fā)生后將要運(yùn)行的代碼,然后把這些代碼放到一個(gè)函數(shù)里,最后把這個(gè)函數(shù)當(dāng)作參數(shù)傳遞給調(diào)用者,稍后由調(diào)用者函數(shù)調(diào)用。
在JavaScript里,一個(gè)函數(shù)并不是個(gè)孤立的定義,它同時(shí)會記住自己被聲明的那個(gè)作用域的上下文,這種機(jī)制讓JavaScript的函數(shù)可以訪問函數(shù)定義所在那個(gè)上下文及父上下文里的所有變量。
當(dāng)你把一個(gè)回調(diào)函數(shù)當(dāng)作參數(shù)傳遞給調(diào)用者后,這個(gè)函數(shù)就會在稍后的某個(gè)時(shí)刻被調(diào)用。即使定義回調(diào)函數(shù)的那個(gè)作用域已經(jīng)結(jié)束,在回調(diào)函數(shù)被調(diào)用時(shí),它依然能夠訪問這個(gè)已結(jié)束的作用域及其父作用域里的所有變量。像最后那個(gè)例子,回調(diào)函數(shù)在jQuery的click()內(nèi)部被調(diào)用,它卻依然能訪問clickCount變量。
前面展現(xiàn)了閉包的神奇之處,把狀態(tài)變量傳遞給一個(gè)函數(shù)就可以讓你不用維護(hù)狀態(tài)就能進(jìn)行事件驅(qū)動(dòng)編程,JavaScript的閉包機(jī)制會幫你維護(hù)它們。
小結(jié)
事件驅(qū)動(dòng)編程是一種通過事件觸發(fā)來決定程序執(zhí)行流程的編程模型。程序員為他們感興趣的事件注冊回調(diào)函數(shù)(通常被稱作事件處理器),然后系統(tǒng)在事件發(fā)生時(shí)調(diào)用已注冊的事件處理器。這種編程模型有很多傳統(tǒng)阻塞編程模型所不具備的優(yōu)勢,以前要實(shí)現(xiàn)類似的特性,就必須使用多進(jìn)程/多線程才行。
JavaScript是種強(qiáng)大的語言,因?yàn)樗牡谝活愋蛯ο蟮暮瘮?shù)和閉包特性,讓它很適合事件驅(qū)動(dòng)編程。
相關(guān)文章
node.js實(shí)現(xiàn)http服務(wù)器與瀏覽器之間的內(nèi)容緩存操作示例
這篇文章主要介紹了node.js實(shí)現(xiàn)http服務(wù)器與瀏覽器之間的內(nèi)容緩存操作,結(jié)合實(shí)例形式分析了node.js http服務(wù)器與瀏覽器之間的內(nèi)容緩存原理與具體實(shí)現(xiàn)技巧,需要的朋友可以參考下2020-02-02詳解如何在Node.js中執(zhí)行CPU密集型任務(wù)
Node.js通常被認(rèn)為不適合CPU密集型應(yīng)用程序,Node.js的工作原理使其在處理I/O密集型任務(wù)時(shí)大放異彩,雖然執(zhí)行CPU密集型任務(wù)肯定不是Node的主要使用場景,但是我們依舊有方法來改善這些問題,本文詳細(xì)給大家介紹了如何在Node.js中執(zhí)行CPU密集型任務(wù)2023-12-12詳解webpack打包nodejs項(xiàng)目(前端代碼)
這篇文章主要介紹了webpack打包nodejs項(xiàng)目(前端代碼),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09在Debian(Raspberry Pi)樹莓派上安裝NodeJS的教程詳解
在樹莓派上運(yùn)行NodeJS并不需要特別的配置,你只需要確??梢杂胦penssh遠(yuǎn)程連接到你的樹莓派就ok了,關(guān)于在Debian(Raspberry Pi)樹莓派上安裝NodeJS的方法,大家可以通過本文了解下2017-09-09