node.js是基於單執行緒無阻塞非同步式的I/O,非同步式的I/O指的是當遇到I/O操作的時候,執行緒不阻塞而是進行下面的操作,那麼I/O操作完成之後,執行緒時如何知道該操作完成的呢?
當操作完成耗時的I/O操作之後,會以事件的形式通知I/O操作的執行緒完成,執行緒會在特定的時候來處理這個事件,進行下一步的操作,為了完成非同步I/O,執行緒必須有事件循環的機制,不停的堅持是否有沒有完成的事件,依序完成這些事件的處理。
而對於阻塞式I/O,執行緒遇到耗時的I/O操作會停止繼續執行,等待操作的完成,這個時候執行緒就不能接受其他的操作請求,為了提供吞吐量,必須建立多個線程,每個線程去回應一個客戶的請求,但是同一時間,一個cpu核心上面只能運行一個線程,多個線程要想執行就必須在不同的線程之間進行切換。
因此node.js少了多線程中線程的創建,以及線程的切換的開銷,線程切換的代價是非常大的,需要為其分配內存,列入調度,同時在線程切換的時候需要執行記憶體換頁等等操作,採用單執行緒的方式就可以減少這些操作。但是這種程式設計方式也有缺點,不符合人們的設計思維。
node.js是基於事件的模式來實現非同步I/O的,當其啟動之後會不停的遍歷是否有為完成的事件,然後進行執行,執行完成之後會以另外一個事件的形式通知線程,本操作已經完成,這個事件又會被添加到未完成的事件列表中,線程在接下來的某個時刻遍歷到這個事件然後進行執行,在這種機制中,需要將一個大的任務分成一個小的事件,node.js也適合處理一些高I/O,低邏輯的場景。
下面的範例示範非同步的檔案讀取:
var fs = require('fs'); fs.readFile('file.txt', 'utf-8', function(err, data) { if (err) { <span style="white-space:pre"> </span>console.error(err); } else { <span style="white-space:pre"> </span>console.log(data); } }); [javascript] view plain copy console.log("end");
如上fs. readFile
非同步讀取文件,之後流程就會繼續走,並不會等待其讀取完文件,當文件讀取完畢之後,會發布一個事件,執行線程遍歷到該事件就會去執行對應的操作,這裡是執行對應的回呼函數,例子中字串end會比檔案內容先列印出來。
node.js的事件API
events.EventEmitter
:EventEmitter對node.js中的事件發射與事件監聽功能提供了封裝,每個事件由一個標識事件名的字串和對應的操作組成。
事件的監聽:
var events = require("events"); var emitter = new events.EventEmitter(); <span style="font-family: Arial, Helvetica, sans-serif;">emitter.on("eventName", function(){</span> console.log("eventName事件发生") })
#事件的發布:
emitter.emit("eventName");
發布事件的時候我們可以傳入多個參數,第一個參數表示事件的名稱,其後的參數表示傳入的參數,這些參數會被傳入到事件的回調函數中。
EventEmitter.once("eventName", listener)
:為事件註冊一個只執行一次的監聽器,當事件第一次發生並觸發監聽器之後,該監聽器就會解除,之後如果事件發生,則該監聽器不會執行。
EventEmitter.removeListener(event, listener)
:移除事件的監聽器
EventEmitter.removeAllListeners(event)
:移除掉事件的所有的監聽器
EventEmitter.setMaxListeners(n)
:node.js預設單一事件最大的監聽器個數是10,如果超過10會給予警告,這麼做是為了防止記憶體的溢出,我們可以改變這個限制設定為其他的數字,如果設定為0表示不進行限制。
EventEmitter.listeners(event)
:傳回某個事件的監聽器清單
多重事件之間協作
在稍微大一點的應用程式中,資料與Web伺服器之間的分離是必然的,例如新浪微博、Facebook、Twitter等。這樣的優勢在於資料來源統一,並且可以為相同資料來源製定各種豐富的客戶端程式。
以Web應用為例,在渲染一張頁面的時候,通常需要從多個資料來源拉取數據,並最終渲染至客戶端。 Node.js在這種場景中可以很自然很方便的同時並行發起對多個資料來源的請求。
api.getUser("username", function (profile) { // Got the profile }); api.getTimeline("username", function (timeline) { // Got the timeline }); api.getSkin("username", function (skin) { // Got the skin });
Node.js透過非同步機制使請求之間無阻塞,達到平行請求的目的,有效的呼叫下層資源。但是,這個場景中的問題是對於多個事件回應結果的協調並非被Node.js原生優雅地支援。
為了達到三個請求都得到結果後才進行下一個步驟,程式也許會被變成以下情況:
api.getUser("username", function (profile) { api.getTimeline("username", function (timeline) { api.getSkin("username", function (skin) { // TODO }); }); });
這將導致請求變成串行進行,無法最大化利用底層的API伺服器。
为解决这类问题,我曾写作一个模块来实现多事件协作,以下为上面代码的改进版:
var proxy = new EventProxy(); proxy.all("profile", "timeline", "skin", function (profile, timeline, skin) { // TODO }); api.getUser("username", function (profile) { proxy.emit("profile", profile); }); api.getTimeline("username", function (timeline) { proxy.emit("timeline", timeline); }); api.getSkin("username", function (skin) { proxy.emit("skin", skin); });
EventProxy也是一个简单的事件侦听者模式的实现,由于底层实现跟Node.js的EventEmitter不同,无法合并进Node.js中。但是却提供了比EventEmitter更强大的功能,且API保持与EventEmitter一致,与Node.js的思路保持契合,并可以适用在前端中。
这里的all方法是指侦听完profile、timeline、skin三个方法后,执行回调函数,并将侦听接收到的数据传入。
利用事件队列解决雪崩问题
所谓雪崩问题,是在缓存失效的情景下,大并发高访问量同时涌入数据库中查询,数据库无法同时承受如此大的查询请求,进而往前影响到网站整体响应缓慢。
那么在Node.js中如何应付这种情景呢。
var select = function (callback) { db.select("SQL", function (results) { callback(results); }); };
以上是一句数据库查询的调用,如果站点刚好启动,这时候缓存中是不存在数据的,而如果访问量巨大,同一句SQL会被发送到数据库中反复查询,影响到服务的整体性能。一个改进是添加一个状态锁。
var status = "ready"; var select = function (callback) { if (status === "ready") { status = "pending"; db.select("SQL", function (results) { callback(results); status = "ready"; }); } };
但是这种情景,连续的多次调用select发,只有第一次调用是生效的,后续的select是没有数据服务的。所以这个时候引入事件队列吧:
var proxy = new EventProxy(); var status = "ready"; var select = function (callback) { proxy.once("selected", callback); if (status === "ready") { status = "pending"; db.select("SQL", function (results) { proxy.emit("selected", results); status = "ready"; }); } };
这里利用了EventProxy对象的once
方法,将所有请求的回调都压入事件队列中,并利用其执行一次就会将监视器移除的特点,保证每一个回调只会被执行一次。对于相同的SQL语句,保证在同一个查询开始到结束的时间中永远只有一次,在这查询期间到来的调用,只需在队列中等待数据就绪即可,节省了重复的数据库调用开销。由于Node.js单线程执行的原因,此处无需担心状态问题。这种方式其实也可以应用到其他远程调用的场景中,即使外部没有缓存策略,也能有效节省重复开销。此处也可以用EventEmitter替代EventProxy,不过可能存在侦听器过多,引发警告,需要调用setMaxListeners(0)
移除掉警告,或者设更大的警告阀值。
以上是Node.js中的事件監聽和事件發布用法實例詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!