做者:Janith Kasun翻譯:瘋狂的技術宅javascript
原文:https://stackabuse.com/handli...前端
未經容許嚴禁轉載java
在本教程中咱們學習 Node.js 的原生 EvenEmitter
類。學完後你將瞭解事件、怎樣使用 EvenEmitter
以及如何在程序中利用事件。另外還會學習 EventEmitter
類從其餘本地模塊擴展的內容,並經過一些例子瞭解背後的原理。node
總之本文涵蓋了關於 EventEmitter
類的全部內容。程序員
當今事件驅動的體系結構很是廣泛,事件驅動的程序能夠產生、檢測和響應各類事件。面試
Node.js 的核心部分是事件驅動的,有許多諸如文件系統(fs
)和 stream
這樣的模塊自己都是用 EventEmitter
編寫的。數據庫
在事件驅動的編程中,事件(event) 是一個或多個動做的結果,這多是用戶的操做或者傳感器的定時輸出等。編程
咱們能夠把事件驅動程序看做是發佈-訂閱模型,其中發佈者觸發事件,訂閱者偵聽事件並採起相應的措施。segmentfault
例如,假設有一個服務器,用戶能夠向其上傳圖片。在事件驅動的編程中,諸如上傳圖片之類的動做將會發出一個事件,爲了利用它,該事件還會有 1 到 n 個訂閱者。數組
在觸發上傳事件後,訂閱者能夠經過向網站的管理員發電子郵件,讓他們知道用戶已上傳照片並對此作出反應;另外一個訂閱者可能會收集有關操做的信息,並將其保存在數據庫中。
這些事件一般是彼此獨立的,儘管它們也多是相互依賴的。
EventEmitter
類是 Node.js 的內置類,位於 events
模塊。根據文檔中的描述:
大部分的 Node.js 核心 API 都是基於慣用的異步事件驅動的體系結構所實現的,在該體系結構中,某些類型的對象(稱爲「發射器」)發出已命名事件,這些事件會致使調用
Function
對象(「監聽器」)」
這個類在某種程度上能夠描述爲發佈-訂閱模型的輔助工具的實現,由於它能夠用簡單的方法幫助事件發送器(發佈者)發佈事件(消息)給 監聽器(訂閱者)。
話雖如此,但仍是先建立一個 EventEmitter
更加實在。能夠經過建立類自己的實例或經過自定義類實現,而後再建立該類的實例來完成。
先從一個簡單的例子開始:建立一個 EventEmitter
,它每秒發出一個含有程序運行時間信息的事件。
首先從 events
模塊中導入 EventEmitter
類:
const { EventEmitter } = require('events');
而後建立一個 EventEmitter
:
const timerEventEmitter = new EventEmitter();
用這個對象發佈事件很是容易:
timerEventEmitter.emit("update");
前面已經指定了事件名,並把它發佈爲事件。可是程序沒有任何反應,由於尚未偵聽器對這個事件作出反應。
先讓這個事件每秒重複一次。用 setInterval()
方法建立一個計時器,每秒發佈一次 update
事件:
let currentTime = 0; // 每秒觸發一次 update 事件 setInterval(() => { currentTime++; timerEventEmitter.emit('update', currentTime); }, 1000);
EventEmitter
實例用來接受事件名稱和參數。把 update
做爲事件名, currentTime
做爲自程序啓動以來的時間進行傳遞。
經過 emit()
方法觸發發射器,該方法用咱們提供的信息推送事件。準備好事件發射器以後,爲其訂閱事件監聽器:
timerEventEmitter.on('update', (time) => { console.log('從發佈者收到的消息:'); console.log(`程序已經運行了 ${time} 秒`); });
經過 on()
方法建立偵聽器,並傳遞事件名稱來指定但願將偵聽器附加到哪一個事件上。 在 update
事件上,運行一個記錄時間的方法。
on()
函數的第二個參數是一個回調,能夠接受事件發出的附加數據。
運行代碼將會輸出:
從發佈者收到的消息: 程序已經運行了 1 秒 從發佈者收到的消息: 程序已經運行了 2 秒 從發佈者收到的消息: 程序已經運行了 3 秒 ...
若是隻在事件首次觸發時才須要執行某些操做,也能夠用 once()
方法進行訂閱:
timerEventEmitter.once('update', (time) => { console.log('從發佈者收到的消息:'); console.log(`程序已經運行了 ${time} 秒`); });
運行這段代碼會輸出:
從發佈者收到的消息: 程序已經運行了 1 秒
下面建立另外一種事件發送器。這是一個計時程序,有三個偵聽器。第一個監聽器每秒更新一次時間,第二個監聽器在計時即將結束時觸發,最後一個在計時結束時觸發:
update
:每秒觸發一次end
:在倒數計時結束時觸發end-soon
:在計時結束前 2 秒觸發先寫一個建立這個事件發射器的函數:
const countDown = (countdownTime) => { const eventEmitter = new EventEmitter(); let currentTime = 0; // 每秒觸發一次 update 事件 const timer = setInterval(() => { currentTime++; eventEmitter.emit('update', currentTime); // 檢查計時是否已經結束 if (currentTime === countdownTime) { clearInterval(timer); eventEmitter.emit('end'); } // 檢查計時是否會在 2 秒後結束 if (currentTime === countdownTime - 2) { eventEmitter.emit('end-soon'); } }, 1000); return eventEmitter; };
這個函數啓動了一個每秒鐘發出一次 update
事件的事件。
第一個 if
用來檢查計時是否已經結束並中止基於間隔的事件。若是已結束將會發布 end
事件。
若是計時沒有結束,那麼就檢查計時是否是離結束還有 2 秒,若是是則發佈 end-soon
事件。
向該事件發射器添加一些訂閱者:
const myCountDown = countDown(5); myCountDown.on('update', (t) => { console.log(`程序已經運行了 ${t} 秒`); }); myCountDown.on('end', () => { console.log('計時結束'); }); myCountDown.on('end-soon', () => { console.log('計時將在2秒後結束'); });
這段代碼將會輸出:
程序已經運行了 1 秒 程序已經運行了 2 秒 程序已經運行了 3 秒 計時將在2秒後結束 程序已經運行了 4 秒 程序已經運行了 5 秒 計時結束
接下來經過擴展 EventEmitter
類來實現相同的功能。首先建立一個處理事件的 CountDown
類:
const { EventEmitter } = require('events'); class CountDown extends EventEmitter { constructor(countdownTime) { super(); this.countdownTime = countdownTime; this.currentTime = 0; } startTimer() { const timer = setInterval(() => { this.currentTime++; this.emit('update', this.currentTime); // 檢查計時是否已經結束 if (this.currentTime === this.countdownTime) { clearInterval(timer); this.emit('end'); } // 檢查計時是否會在 2 秒後結束 if (this.currentTime === this.countdownTime - 2) { this.emit('end-soon'); } }, 1000); } }
能夠在類的內部直接使用 this.emit()
。另外 startTimer()
函數用於控制計時開始的時間。不然它將在建立對象後當即開始計時。
建立一個 CountDown
的新對象並訂閱它:
const myCountDown = new CountDown(5); myCountDown.on('update', (t) => { console.log(`計時開始了 ${t} 秒`); }); myCountDown.on('end', () => { console.log('計時結束'); }); myCountDown.on('end-soon', () => { console.log('計時將在2秒後結束'); }); myCountDown.startTimer();
運行程序會輸出:
程序已經運行了 1 秒 程序已經運行了 2 秒 程序已經運行了 3 秒 計時將在2秒後結束 程序已經運行了 4 秒 程序已經運行了 5 秒 計時結束
on()
函數的別名是 addListener()
。看一下 end-soon
事件監聽器:
myCountDown.on('end-soon', () => { console.log('計時將在2秒後結束'); });
也能夠用 addListener()
來完成相同的操做,例如:
myCountDown.addListener('end-soon', () => { console.log('計時將在2秒後結束'); });
此函數將以數組形式返回全部活動的偵聽器名稱:
const myCountDown = new CountDown(5); myCountDown.on('update', (t) => { console.log(`程序已經運行了 ${t} 秒`); }); myCountDown.on('end', () => { console.log('計時結束'); }); myCountDown.on('end-soon', () => { console.log('計時將在2秒後結束'); }); console.log(myCountDown.eventNames());
運行這段代碼會輸出:
[ 'update', 'end', 'end-soon' ]
若是要訂閱另外一個事件,例如 myCount.on('some-event', ...)
,則新事件也會添加到數組中。
這個方法不會返回已發佈的事件,而是返回訂閱的事件的列表。
這個函數能夠從 EventEmitter
中刪除已訂閱的監聽器:
const { EventEmitter } = require('events'); const emitter = new EventEmitter(); const f1 = () => { console.log('f1 被觸發'); } const f2 = () => { console.log('f2 被觸發'); } emitter.on('some-event', f1); emitter.on('some-event', f2); emitter.emit('some-event'); emitter.removeListener('some-event', f1); emitter.emit('some-event');
在第一個事件觸發後,因爲 f1
和 f2
都處於活動狀態,這兩個函數都將被執行。以後從 EventEmitter
中刪除了 f1
。當再次發出事件時,將會只執行 f2
:
f1 被觸發 f2 被觸發 f2 被觸發
An alias for removeListener()
is off()
. For example, we could have written:
removeListener()
的別名是 off()
。例如能夠這樣寫:
emitter.off('some-event', f1);
該函數用於從 EventEmitter
的全部事件中刪除全部偵聽器:
const { EventEmitter } = require('events'); const emitter = new EventEmitter(); const f1 = () => { console.log('f1 被觸發'); } const f2 = () => { console.log('f2 被觸發'); } emitter.on('some-event', f1); emitter.on('some-event', f2); emitter.emit('some-event'); emitter.removeAllListeners(); emitter.emit('some-event');
第一個 emit()
會同時觸發 f1
和 f2
,由於它們當時正處於活動狀態。刪除它們後,emit()
函數將發出事件,但沒有偵聽器對此做出響應:
f1 被觸發 f2 被觸發
若是要在 EventEmitter
發出錯誤,必須用 error
事件名來完成。這是 Node.js 中全部 EventEmitter
對象的標準配置。這個事件必須還要有一個 Error
對象。例如能夠像這樣發出錯誤事件:
myEventEmitter.emit('error', new Error('出現了一些錯誤'));
error
事件的偵聽器都應該有一個帶有一個參數的回調,用來捕獲 Error
對象並處理。若是 EventEmitter
發出了 error
事件,可是沒有訂閱者訂閱 error
事件,那麼 Node.js 程序將會拋出這個 Error
。這會致使 Node.js 進程中止運行並退出程序,同時在控制檯中顯示這個錯誤的跟蹤棧。
例如在 CountDown
類中,countdownTime
參數的值不能小於 2,不然會沒法觸發 end-soon
事件。在這種狀況下應該發出一個 error
事件:
class CountDown extends EventEmitter { constructor(countdownTime) { super(); if (countdownTimer < 2) { this.emit('error', new Error('countdownTimer 的值不能小於2')); } this.countdownTime = countdownTime; this.currentTime = 0; } // ........... }
處理這個錯誤的方式與其餘事件相同:
myCountDown.on('error', (err) => { console.error('發生錯誤:', err); });
始終對 error
事件進行監聽是一種很專業的作法。
Node.js 中許多原生模塊擴展了EventEmitter
類,所以它們自己就是事件發射器。
一個典型的例子是 Stream
類。官方文檔指出:
流能夠是可讀的、可寫的,或二者都可。全部流都是
EventEmitter
的實例。
先看一下經典的 Stream 用法:
const fs = require('fs'); const writer = fs.createWriteStream('example.txt'); for (let i = 0; i < 100; i++) { writer.write(`hello, #${i}!\n`); } writer.on('finish', () => { console.log('All writes are now complete.'); }); writer.end('This is the end\n');
可是,在寫操做和 writer.end()
調用之間,咱們添加了一個偵聽器。 Stream
在完成後會發出一個 finished
事件。在發生錯誤時會發出 error
事件,把讀取流經過管道傳輸到寫入流時會發出 pipe
事件,從寫入流中取消管道傳輸時,會發出 unpipe
事件。
另外一個類是 child_process
類及其 spawn()
方法:
const { spawn } = require('child_process'); const ls = spawn('ls', ['-lh', '/usr']); ls.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); ls.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); ls.on('close', (code) => { console.log(`child process exited with code ${code}`); });
當 child_process
寫入標準輸出管道時,將會觸發 stdout
的 data
事件。當輸出流遇到錯誤時,將從 stderr
管道發送 data
事件。
最後,在進程退出後,將會觸發 close
事件。
事件驅動的體系結構使咱們可以建立高內聚低耦合的系統。事件表示某個動做的結果,能夠定義 1個或多個偵聽器並對其作出反應。
本文深刻探討了 EventEmitter
類及其功能。對其進行實例化後直接使用,並將其行爲擴展到了一個自定義對象中。
最後介紹了該類的一些重要函數。