用 EventEmitter 處理 Node.js 中的事件

做者: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?

EventEmitter 類是 Node.js 的內置類,位於 events 模塊。根據文檔中的描述:

大部分的 Node.js 核心 API 都是基於慣用的異步事件驅動的體系結構所實現的,在該體系結構中,某些類型的對象(稱爲「發射器」)發出已命名事件,這些事件會致使調用 Function 對象(「監聽器」)」

這個類在某種程度上能夠描述爲發佈-訂閱模型的輔助工具的實現,由於它能夠用簡單的方法幫助事件發送器(發佈者)發佈事件(消息)給 監聽器(訂閱者)。

建立 EventEmitters

話雖如此,但仍是先建立一個 EventEmitter 更加實在。能夠經過建立類自己的實例或經過自定義類實現,而後再建立該類的實例來完成。

建立 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 秒

EventEmitter 與多個監聽器

下面建立另外一種事件發送器。這是一個計時程序,有三個偵聽器。第一個監聽器每秒更新一次時間,第二個監聽器在計時即將結束時觸發,最後一個在計時結束時觸發:

  • 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

接下來經過擴展 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秒後結束');
});

EventEmitter 的主要函數

eventNames()

此函數將以數組形式返回全部活動的偵聽器名稱:

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', ...),則新事件也會添加到數組中。

這個方法不會返回已發佈的事件,而是返回訂閱的事件的列表。

removeListener()

這個函數能夠從 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');

在第一個事件觸發後,因爲 f1f2 都處於活動狀態,這兩個函數都將被執行。以後從 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);

removeAllListeners()

該函數用於從 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() 會同時觸發 f1f2,由於它們當時正處於活動狀態。刪除它們後,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 事件進行監聽是一種很專業的作法。

使用 EventEmitter 的原生模塊

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 寫入標準輸出管道時,將會觸發 stdoutdata 事件。當輸出流遇到錯誤時,將從 stderr 管道發送 data 事件。

最後,在進程退出後,將會觸發 close 事件。

總結

事件驅動的體系結構使咱們可以建立高內聚低耦合的系統。事件表示某個動做的結果,能夠定義 1個或多個偵聽器並對其作出反應。

本文深刻探討了 EventEmitter 類及其功能。對其進行實例化後直接使用,並將其行爲擴展到了一個自定義對象中。

最後介紹了該類的一些重要函數。

173382ede7319973.gif


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索