DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> Node.js中的事件驅動編程詳解
Node.js中的事件驅動編程詳解
編輯:關於JavaScript     

在傳統程編程模裡,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成功的核心。

下面的例子將展示在Web浏覽器裡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() {

                                     clickCount ++;

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

                            });

                   }());


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

閉包如何幫助異步編程

在事件驅動編程模型裡,先編寫事件發生後將要運行的代碼,然後把這些代碼放到一個函數裡,最後把這個函數當作參數傳遞給調用者,稍後由調用者函數調用。

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

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

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

小結

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

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

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved