###【本文是基礎內容,大神請繞道,才疏學淺,不免紕漏,請各位輕噴】 ##1. 概述 目前開源社區最火熱的技術當屬Node.js莫屬了,做爲使用Javascript爲主要開發語言的服務器端編程技術和平臺,一開始就註定會引人矚目。 固然可以吸引衆人的目光,確定不是三教九流之輩,必然擁有獨特的優點和魅力,才能引發羣猿追逐。其中當屬異步IO和事件編程模型,本文據Node.js的異步IO和事件編程作深刻分析。 ##2. 什麼是異步 同步和異步是一個比較早的概念,大抵在操做系統發明時應該就出現了。舉一個最簡單的生活中的例子,好比發短信的狀況會比較好說明他們的區別: 同步:正在處於苦逼工做狀態中的我,但狗屎運的交到了女友並正處於處於熱戀期,所以發送短信給她詢問那個餐廳吃飯,急不可耐的看着手機等待短信回覆,收到信息看完是否加班或者下班; 異步:正處於公司運營決策關鍵工做狀態中的你,不能夠被打斷過久,隨便發送了一條詢問老婆何時作好晚飯而後吃飯的短信後立馬返回工做,一邊工做一邊等待短信回覆通知,根據通知決定是否再工做和下班。 由此能夠看出,同步和異步的特色是:javascript
##3. 爲何須要異步 知其然,還要知其因此然,讀者可能會問,爲何存在異步?根據上面發短信和磁盤操做的例子,答案很明顯,爲了提升辦事的效率,CPU計算速度和磁盤的讀寫速度差太遠了,磁盤供不該求,所以有了計算機的存儲系統的分層設計,平衡了效率和成本。能夠說懶惰推進人類的進步,任何能夠下降花費時間而達到同等功效的方法確定會被優先採用。發送短信時等待對方回覆的時間純粹的浪費掉了,CPU寫入磁盤等待返回的結果的等待時間也被無情的消耗了,這是一個講究效率的時代徹底不能忍受的,所以讓員工一直處於忙碌狀態,最大限度的榨取員工價值是老闆追求的,讓CPU和磁盤都不停的滿負荷處理事務也是效率須要的。所以,異步處理出現了。前端
##4. Node.js異步IO與事件 初次接觸Node.js,恐怕任何人都會被先先灌輸的第一條Node.js就不同凡響的地方:異步IO和事件驅動。毫無疑問,這確實是Node.js最使人津津樂道的特點之處,也是本文重點分析的地方。 ###4.1 Node.js異步機制 因爲異步的高效性,node.js設計之初就考慮作爲一個高效的web服務器,做者理所固然地使用了異步機制,並貫穿於整個node.js的編程模型中,新手在使用node.js編程時,每每會羈絆於因爲其餘編程語言的習慣,好比C/C++,以爲無所適從。咱們能夠從如下一段簡單的睡眠程序代碼窺視出他們的區別,下面是摘自《linux程序設計》打印10個時間的C代碼:java
#include <time.h> #include <stdio.h> #include <unistd.h> int main() { int i; time_t the_time; for(i = 1; i <= 10; i++) { the_time = time((time_t *)0); printf("The time is %ld\n", the_time); sleep(2); } exit(0); }
編譯後打印結果以下: The time is 1396492137 The time is 1396492139 The time is 1396492141 The time is 1396492143 The time is 1396492145 The time is 1396492147 The time is 1396492149 The time is 1396492151 The time is 1396492153 The time is 1396492155 從C語言的打印結果能夠發現,是隔2秒打印一次,按照C程序該有的邏輯,代碼逐行執行。如下Node.js代碼本意如同上述C代碼,使用目的隔2秒打印一次時間,共打印10條(初次從C/C++轉來接觸Node.js的程序員可能會寫出下面的代碼):node
function test() { for (var i = 0; i < 10; i++) { console.log(new Date); setTimeout(function(){}, 2000); //睡眠2秒,而後再進行一下次for循環打印 } }; test();
打印結果: Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
Tue Apr 01 2014 14:53:22 GMT+0800 (中國標準時間)
觀察結果發現都是在14:53:22同一個時間點打印的,根本就沒有睡眠2秒後再執行下一輪循環打印!這是爲何?從官方的文檔咱們看出setTimeout是第二個參數表示逝去時間以後在執行第一個參數表示的callback函數,所以咱們能夠分析, 因爲Node.js的異步機制,setTimeout每一個for循環到此以後,都註冊了一個2秒後執行的回調函數而後當即返回立刻執行console.log(new Date),致使了全部打印的時間都是同一個點,所以咱們修改for循環的代碼以下:linux
for (var i = 0; i < 10; i++) { setTimeout(function(){ console.log(new Date); }, 2000); }
執行結果以下所示: Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:30:35 GMT+0800 (中國標準時間) 神奇,仍然是同一個時間點,見鬼!冷靜下來分析,時刻考慮異步,for循環裏每次setTimeout註冊了2秒以後執行的一個打印時間的回調函數,而後當即返回,再執行setTimeout,如此反覆直到for循環結束,由於執行速度太快,致使同一個時間點註冊了10個2秒後執行的回調函數,所以致使了2秒後全部回調函數的當即執行。 咱們在for循環以前添加console.log("before FOR: " + new Date)和以後console.log("after FOR: " + new Date),來驗證咱們的推測,打印結果以下(後面省略8條相同的打印行): before FOR: Thu Apr 03 2014 09:42:43 GMT+0800 (中國標準時間)
after FOR: Thu Apr 03 2014 09:42:43 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:42:45 GMT+0800 (中國標準時間)
Thu Apr 03 2014 09:42:45 GMT+0800 (中國標準時間) …… (省略與上一行8條相同的打印行) 由此能夠窺視出Node.js異步機制的端倪了,在for循環中的代碼於其後的代碼幾乎在一個單位秒內完成,而定時器中的回調函數則按要求的2秒以後執行,也是同一秒內執行完畢。那麼如何實現最初C語言每隔2秒打印一個系統時間的需求函數呢,我實現了以下一個wsleep函數,放在for循環中,能夠達到該目的:ios
function wsleep(milliSecond) { var startTime = new Date().getTime(); while(new Date().getTime() <= milliSecond + startTime) { } }
可是該函數有一個令他沒法在項目中使用的缺陷,請問爲何? ###4.2 Node.js事件編程 事件編程並非一個新的概念,作過界面UI編程的程序猿們能夠以爲事件再熟悉不過了,特別是客戶端開發和web開發的感觸頗深吧,如Android、ios、或是javascript前端編程的工程師們,一個按鈕、一個列表項、一個長按操做等等,每次按下都會由操做系統或者瀏覽器產生一個事件,你須要作的工做就是編寫和註冊這個事件的回調函數(可能各自領域內不稱爲回調函數,可是從操做系統的角度考慮其實就是一個回調函數),當這個事件發生時,執行你的回調函數。Node.js不同凡響的時,它基因裏就是由事件和異步組成的。請看用於生產環境中的真實項目代碼的一個片斷(略去了一些不相關的代碼),我加上一段關於事件信息的註釋,讓讀者更清晰:程序員
self.sio.sockets.on('connection', function(socket) { //監聽socket鏈接事件 var addr = socket.handshake.address; var limiter = new RateLimiter(constant.RL_MAXREQRATELIMIT, constant.RL_RATELIMITUNIT, true); var connect = new Connection(socket); then(function(defer) { if (ipLimit) { throttle.throttleHandle(connect, null, defer); //結果回調處理事件 } else { defer(null); //發送處理結果事件 } }).all(function(defer) { //收處處理結果事件 socket.on('message', function(data) { //監聽數據傳輸事件 cloudKeyMain(connect, 1, data, cloudKeyApi); }); }); socket.on("disconnect", function(data) { //監聽socket離線事件 var currentSockClient = connect.client; if (currentSockClient) { currentSockClient.signalOffline(); //發送客戶端離線事件 } }); });
從上面的代碼,咱們能夠看出Node.js無所不在的事件機制,事件機制讓咱們專一與代碼業務的處理流程,提升了軟件開發的效率,下降了代碼之間的耦合,讓人不被雜事纏繞,編程更有趣。如何開始一個簡單的Node.js事件編程呢,答案是使用Node.js的javascript API核心模塊events的events.EventEmitter類便可完成,下面以一個QQ的在線和離線來講明, 事件機制的使用主要包括3個方面的內容:web
var events = require('events'); var util = require('util'); function MyQQ() { events.EventEmitter.call(this); //…… } util.inherits(MyQQ, events.EventEmitter);
OK,上述代碼就完成了事件機制的添加,此時,咱們的工做爲QQ添加事件註冊函數進行事件的註冊,事件註冊主要是使用EventEmitter的on()完成,由於咱們繼承了EventEmitter,能夠直接使用on函數,咱們在on函數的第二個參數callback函數中自定義處理業務,並註冊本身的上線事件,如下是一個QQ上線時簡單的處理業務:編程
function onlineHandle(QQNumber) { //獲取和QQNumber的聯繫人列表 //獲取離線消息 //…… } var myQQ = new MyQQ(); myQQ.on(「onLine」, onlineHandle);
上述代碼完成了事件的處理,下面輪到在何時發佈這個事件,下述的一個業務場景中多是須要發佈該事件的,發佈事件用emit()函數:設計模式
function main() { //鏈接服務器 //檢測登陸狀態 //登陸服務器成功後發佈事件 myQQ.emit(「onLine」,123655245); }
上述myQQ.emit()函數執行後發佈了onLine事件後,會當即執行onlineHandle()函數,處理咱們註冊的業務邏輯,須要注意的是,事件發佈函數emit第二個參數後的參數個數須要和咱們註冊時的處理函數參數個數相同而且順序一致才能正確處理,爲何有這樣的要求?這須要從Node.js事件的原理提及。基本上全部的事件機制都是用設計模式中觀察者模式實現,觀察着模式網絡資料一大堆,如何想要深刻了解的話能夠網絡搜索或者閱讀權威書籍,能夠參考《設計模式:可複用面向對象軟件的基礎》和《Head First設計模式》。
原文:http://cnodejs.org/topic/533d6edbc2621e680800e0ea