首頁  >  文章  >  web前端  >  Node.js中的事件驅動程式設計詳解_node.js

Node.js中的事件驅動程式設計詳解_node.js

WBOY
WBOY原創
2016-05-16 16:39:371265瀏覽

在傳統程程式模裡,I/O操作就像一個普通的本地函數呼叫:在函數執行完之前程式被阻塞,無法繼續運作。阻塞I/O起源於早先的時間片模型,這種模型下每個進程就像一個獨立的人,目的是將每個人區分開,而且每個人在同一時刻通常只能做一件事,必須等待前面的事做完才能決定下一件事要做什麼。但是這種在電腦網路和Internet上被廣泛使用的「一個用戶,一個進程」的模型伸縮性很差。管理多個進程時,會耗費很多內存,上下文切換也會佔用大量資源,這些對作業系統是個很大的負擔,而且隨著進程數的遞增,會導致系統效能急劇衰減。

多線程是個替代方案,線程是一個輕量級的進程,它會和同一個進程內的其它線程共享內存,它更像傳統模型的擴展,用來並發執行多個線程,當一個線程等待I/O操作時,其它執行緒可以接管CPU,當I/O操作完成,前面等待的執行緒會被喚醒。是說,一個運行中的執行緒可以被中斷,然後稍候再被恢復。此外,在一些系統下執行緒可以在多核心CPU的不同核心下並行運行。

程式設計師並不知道線程會在什麼具體時間運行,他們必須很小心的處理共享內存的並發訪問,因此必須使用一些同步原語來同步訪問某個數據結構,比如使用鎖或信號量,以此來強制線程以特定的行為和計劃執行。那些大量依賴線程間的共享狀態的應用程序,很容易就會出現一些隨機性很強,難以查找的奇怪問題。

還有一種方式是使用多執行緒協作,由你自己負責顯式的釋放CPU,並把CPU時間交給其他執行緒使用,因為由你親自來控制執行緒的執行計劃,因此減小了對同步的需求,但是也提高了程式的複雜度和出錯的機會,而且並沒有避免多執行緒的那些問題。

什麼是事件驅動程式設計

事件驅動程式設計(Evnet-driven programming)是一種程式設計風格,由事件來決定程式的執行流程,事件由事件處理器(event handler)或事件回呼(event callback)來處理,事件回呼是當某個特定事件發生時被呼叫的函數,例如資料庫回傳了查詢結果或使用者點擊了一個按鈕。

回想下,在傳統的阻塞I/O程式模式裡,資料庫查詢可能像這樣:

複製程式碼 程式碼如下:

result = query('SELECT * FROM posts WHERE id = 1');

do_something_with(result);


上面的query函數會讓目前執行緒或程序一直處於等待狀態,直到底層資料庫完成查詢操作並傳回。

在事件驅動模型裡,這個查詢會變成這樣:

複製程式碼 程式碼如下:

query_finished = function(result) {

        do_something_with(result);

}

query('SELECT * FROM posts WHERE id = 1', query_finished);

   首先你定義了一個叫query_finished的函數,它包含了查詢完成後要做的事。然後把這個函數當作參數傳遞給query函數,當query執行完畢會呼叫query_finished,而不是只回傳查詢結果。

當你感興趣的事件發生時會呼叫你定義的函數,而不是簡單的回傳結果值,這種程式設計模型就叫事件驅動程式設計或非同步程式設計。這是Node一個最明顯的特性,這種程式設計模型意味著當前進程在執行I/O操作時不會被阻塞,因此,多個I/O操作可以並行執行,當操作完成後相應的回調函數就會被呼叫。

事件驅動程式設計底層依賴事件循環(event loop),而事件循環基本上是事件偵測和事件處理器觸發這兩種函數不斷循環呼叫的一個結構。在每次循環裡,事件循環機制需要偵測發生了哪些事件,當事件發生時,它找到對應的回調函數並呼叫它。

事件循環只是運行在進程內的一個線程,當事件發生時,事件處理器可以單獨運行並且不會被中斷,也就是說:

1.在某個特定時刻最多有一個事件回呼函數在運作
2.任何事件處理器運作時都不會被中斷

有了這個,開發人員就可以不再為線程同步和並發修改共享內存這些事頭疼了。

一個眾所周知的秘密:

很久以前,系統程式設計社群的人們就知道事件驅動程式設計是創建高並發服務最佳方式,因為它不用保存很多上下文,因此節省了大量內存,也沒有那麼多上下文切換,又節省了大量執行時間。

慢慢的,這種理念滲透到了其他的平台和社區,出現了一些有名的事件循環實現,比如Ruby的Event machine,Perl的AnyEvnet,以及Python的Twisted,除了這些還有很多其它的實現和語言。

用這些框架做開發,需要學習框架相關的特定知識以及框架特定的類別庫,例如,使用Event Machine時,為了享受非阻塞帶來的好處,你得避免使用同步類別庫,只能用Event Machine的非同步類別庫。如果你使用了任何阻塞類別庫(例如Ruby的大多數標準庫),你的伺服器就失去了最佳的伸縮性,因為事件循環依然會不斷地被阻塞,時不時地阻礙了I/O事件的處理。

Node最初就被設計成一個非阻塞I/O伺服器平台,因此一般情況下,你應該期望運行在它上面的所有程式碼都是非阻塞的。因為JavaScript非常小,而且它不會強制使用任何I/O模型(因為它沒有標準的I/O類別函式庫),所以Node建立在一個很純淨的環境裡,不會有任何歷史遺留問題。

Node和JavaScript如何簡化了非同步應用程式

Node的作者Ryan Dahl,最初使用C來開發這個項目,但是發現維護函數呼叫的上下文太複雜,導致程式碼複雜度很高。然後他轉用Lua,但是Lua已經有個幾個阻塞的I/O類庫,阻塞和非阻塞混在一起可能會讓開發人員很迷惑並因此阻礙了很多人構建可伸縮的應用,於是Lua也被Dahl拋棄了。最後他轉向了JavaScript,JavaScript中的閉包及第一級物件的函數,這些特性使JavaScript非常適合用作事件驅動程式設計。 JavaScript的魔力是讓Node如此流行的一個主要原因。

什麼是閉包

閉包可以理解為一個特殊的函數,但是它可以繼承並存取它自身被定義的那個作用域裡的變數。當你將一個回調函數作為參數傳遞給另外一個函數時,它稍候會被調用,神奇的是,這個回調函數被稍候調用時,它居然記住了它自身定義所在的那個上下文以及父上下文裡面的變量,而且還可以正常存取它們。這個強大的特性是Node成功的核心。

下面的範例將顯示在網頁瀏覽器裡JavaScript閉包是如何運作的。假如,你要監聽一個按鈕的單機事件,你可以這樣做:

複製程式碼 程式碼如下:

var clickCount = 0;

document.getElementById('myButton').onclick = function() {

        clickCount = 1;

        alert("clicked " clickCount " times.");

};

使用jQuery時是這樣:

複製程式碼 程式碼如下:

var clickCount = 0;

$('button#mybutton').click(function() {

        clickedCount ;

        alert('Clicked ' clickCount ' times.');

});

JavaScript裡,函數是第一類對象,就是說你可以把函數當作參數來傳遞給其他函數。上面的兩個例子,前者把一個函數賦值給另一個函數,後者把函數當作參數傳遞給另一個函數,點選事件的處理函數(回呼函數)可以存取函數定義所在程式碼區塊下的每個變數,在這個例子裡,它可以存取在它父閉包內定義的clickCount變數。

clickCount變數處在全域作用域(JavaScript裡最外層的作用域),它保存了使用者點擊按鈕的次數,通常在全域作用域下儲存變數是個壞習慣,因為那樣很容易跟其他程式碼衝突,你應該把變數放在使用它們的本地作用域裡。大多時候,只用把程式碼用一個函數包裝起來,等於另外創建了閉包,這樣就可以很容易避免污染全局環境,就像這樣:

複製程式碼 程式碼如下:

                  (function() {

                           var clickCount = 0;

                            $('button#mybutton').click(function() {

                                  

                                  

                            });

                   }());


  注意:上面程式碼的第七行,定義了一個函數後立刻呼叫它,這是JavaScript裡一個常見的設計模式:透過建立函數來建立一個新的作用域。


閉包如何幫助非同步程式設計

在事件驅動程式設計模型裡,先寫事件發生後將要執行的程式碼,然後把這些程式碼放到一個函數裡,最後把這個函數當作參數傳遞給呼叫者,稍後由呼叫者函數呼叫。

在JavaScript裡,一個函數並不是個孤立的定義,它同時會記住自己被宣告的那個作用域的上下文,這種機制讓JavaScript的函數可以存取函數定義所在那個上下文及父上下文裡的所有變數。

當你把一個回呼函數當作參數傳遞給呼叫者後,這個函數就會在稍後的某個時刻被呼叫。即使定義回呼函數的那個作用域已經結束,在回呼函數被呼叫時,它依然能夠存取這個已結束的作用域及其父作用域裡的所有變數。像最後那個例子,回呼函數在jQuery的click()內部被調用,但它仍然可以存取clickCount變數。

前面展現了閉包的神奇之處,把狀態變數傳遞給一個函數就可以讓你不用維護狀態就能進行事件驅動編程,JavaScript的閉包機制會幫你維護它們。

小結

事件驅動程式設計是一種透過事件觸發來決定程式執行流程的程式設計模型。程式設計師為他們感興趣的事件註冊回調函數(通常被稱為事件處理器),然後系統在事件發生時呼叫已註冊的事件處理器。這種程式設計模型有許多傳統阻塞程式設計模型所不具備的優勢,以前要實現類似的特性,就必須使用多進程/多執行緒才行。

JavaScript是種強大的語言,因為它的第一個類型物件的函數和閉包特性,讓它很適合事件驅動程式設計。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn