EventEmitter class

在Node中,要實(shí)現(xiàn)觀察者模式非常的簡單,而且內(nèi)置于EventEmitter類中,EventEmitter類允許我們注冊一個(gè)或者多個(gè)函數(shù)作為監(jiān)聽者,當(dāng)對應(yīng)的事件觸發(fā)后,它們就會被觸發(fā)

64C16489-985B-4083-823C-0A426D4F3C68.png

EventEmitter是一個(gè)原型,可以通過events這個(gè)核心模塊獲取得到

const EventEmitter = require('events').EventEmitter;
const eventEmitter = new EventEmitter();

EventEmitter內(nèi)部提供了幾個(gè)API

  • on(event, listener) 注冊監(jiān)聽者
  • once(event, listener)注冊監(jiān)聽者,但是只會觸發(fā)一次
  • emit(event, [arg1], [...]) 發(fā)布一個(gè)事件
  • removeListener(event, listener)移除監(jiān)聽者

上面的方法都支持鏈?zhǔn)秸{(diào)用。而且我們會發(fā)現(xiàn)listener和我們所知道的傳統(tǒng)的Node回調(diào)函數(shù)是不同的,最突出的就是函數(shù)的第一個(gè)參數(shù)不是error,而是我們emit時(shí)候穿過去的參數(shù)

使用EventEmitter

使用它最簡單的方式就是去創(chuàng)建一個(gè)新的實(shí)例,然后立刻使用它

const EventEmitter = require('events').EventEmitter;
const fs = require('fs');

function findPattern(files, regex) {
    const emitter = new EventEmitter();
    files.forEach((file, index) => {
        fs.readFile(file, (err, content) => {
            if(err) {
                emitter.emit('error', err);
            }
            emitter.emit('fileRead', file);
            if(regex.test(content)) {
                emitter.emit('found', content);
            }
        })
    })
    return emitter;
}

findPattern(['a.js', 'b.js', 'main.js'], new RegExp("main", 'i'))
    .on('error', function(err) {
        console.log(err);
    })
    .on('fileRead', function(file) {
        console.log(file + ' read\n');
    })
    .on('found', function(content) {
        console.log(content.toString());
        console.log('\n');
    })

傳播錯(cuò)誤

EventEmitter和回調(diào)函數(shù)一樣,當(dāng)異步事件發(fā)生錯(cuò)誤的時(shí)候,直接拋出異常是無法被捕捉到的,因?yàn)樗鼈兯幍?code>event loop是不相同的

const EventEmitter = require('events').EventEmitter;
const fs = require('fs');

function findPattern(files, regex) {
    const emitter = new EventEmitter();
    files.forEach((file, index) => {
        fs.readFile(file, (err, content) => {
            if(err) {
                // emitter.emit('error', err);
                throw new Error(err);
            }
            emitter.emit('fileRead', file);
            if(regex.test(content)) {
                emitter.emit('found', content);
            }
        })
    })
    return emitter;
}

findPattern(['a.js', 'b.js', 'main.js'], new RegExp("main", 'i'))
    .on('error', function(err) {
        console.log(err);
    })
    .on('fileRead', function(file) {
        console.log(file + ' read\n');
    })
    .on('found', function(content) {
        console.log(content.toString());
        console.log('\n');
    })

所以我們平時(shí)在寫代碼的時(shí)候,要注意,多增加一個(gè)關(guān)于error的事件進(jìn)行監(jiān)聽,然后進(jìn)一步處理

使任何對象可觀察

有些時(shí)候,我們直接通過EventEmitter去創(chuàng)建一個(gè)可觀察對象是不夠的,因?yàn)樗荒軌驗(yàn)槲覀兲峁U(kuò)展的功能,所以更多的時(shí)候,我們通過繼承EventEmitter這個(gè)類,去創(chuàng)建一個(gè)更具有擴(kuò)展性的可觀察的對象

為了進(jìn)一步描述這種方法,我們通過重寫上面的findPattern來說明

const fs = require('fs');
const EventEmitter = require('events').EventEmitter;

class FindPattern extends EventEmitter {
    constructor(regex) {
        super();
        this.regex = regex;
        this.files = [];
    }

    addFile(file) {
        this.files.push(file);
        return this;
    }

    find() {
        this.files.forEach((file, index) => {
            fs.readFile(file, (err, content) => {
                if(err) {
                    this.emit('error', err);
                }
                this.emit('fileRead', file);
                if(this.regex.test(content)) {
                    this.emit('found', content);
                }
            })
        })
    }
}



const interface = new FindPattern(new RegExp("main"));

interface.addFile('a.js')
    .addFile('b.js')
    .addFile('main.js')
    .on('error', function(err) {
        console.log(err);
    })
    .on('fileRead', function(file) {
        console.log(file + ' read\n');
    })
    .on('found', function(content) {
        console.log(content.toString());
        console.log('\n');
    })
    .find()

這種模式在Node的生態(tài)圈里十分常見,HTTP、TCP等模塊里都大量了使用了這種模式

同步和異步事件

和回調(diào)函數(shù)一樣,事件是可以同步觸發(fā),也可以是異步觸發(fā)的,但是強(qiáng)調(diào)的是,在同一個(gè)EventEmitter內(nèi),不能混用兩種模式

emit同步事件和異步事件最主要的區(qū)別取決于注冊listener的方法,如果events是異步的emit,那么程序有充足的時(shí)間去注冊listener,因?yàn)閑vent不會在本次event loop被觸發(fā)

如果events是同步的被emit,那么就需要在emit之前去注冊listener

class SycnEmitter extends EventEmitter {
    constructor() {
        super();
        this.emit('ready', 'ready');
    }
}

const interface = new SycnEmitter();

interface.on('ready', (ready) => {
    console.log(ready);
})

上面的代碼,就是因?yàn)闆]有在emit之前進(jìn)行注冊,導(dǎo)致沒有任何的輸出結(jié)果,如果是異步的去emit的話,就不會這樣。

所以我們在使用EventEmitter的時(shí)候,需要考慮好使用的場景,再根據(jù)場景決定使用同步的方式還是異步的方式

EventEmitter VS callbacks

在定義異步API時(shí),常見的難點(diǎn)是檢查是否使用EventEmitter的事件機(jī)制或僅接受回調(diào)函數(shù)。一般區(qū)分規(guī)則是這樣的:當(dāng)一個(gè)結(jié)果必須以異步方式返回時(shí),應(yīng)該使用回調(diào)函數(shù),當(dāng)需要結(jié)果不確定其方式時(shí),應(yīng)該使用事件機(jī)制來響應(yīng)。

但是,由于這兩者實(shí)在太相近,并且可能兩種方式都能實(shí)現(xiàn)相同的應(yīng)用場景,所以產(chǎn)生了許多混亂。以下列代碼為例:

function helloEvents() {
  const eventEmitter = new EventEmitter();
  setTimeout(() => eventEmitter.emit('hello', 'hello world'), 100);
  return eventEmitter;
}

function helloCallback(callback) {
  setTimeout(() => callback('hello world'), 100);
}

helloEvents()helloCallback()在其功能上可以被認(rèn)為是等價(jià)的,第一個(gè)使用事件機(jī)制實(shí)現(xiàn),第二個(gè)則使用回調(diào)來通知調(diào)用者,而將事件作為參數(shù)傳遞。但是真正區(qū)分它們的是可執(zhí)行性,語義和要實(shí)現(xiàn)或使用的代碼量。雖然我們不能給出一套確定性的規(guī)則來選擇一種風(fēng)格,但我們當(dāng)然可以提供一些提示來幫助你做出決定。

相比于第一個(gè)例子,即觀察者模式而言,回調(diào)函數(shù)在支持不同類型的事件時(shí)有一些限制。但是事實(shí)上,我們?nèi)匀豢梢酝ㄟ^將事件類型作為回調(diào)的參數(shù)傳遞,或者通過接受多個(gè)回調(diào)來區(qū)分多個(gè)事件。然而,這樣做的話不能被認(rèn)為是一個(gè)優(yōu)雅的API。在這種情況下,EventEmitter可以提供更好的接口和更精簡的代碼。

EventEmitter更優(yōu)秀的另一種應(yīng)用場景是多次觸發(fā)同一事件或不觸發(fā)事件的情況。事實(shí)上,無論操作是否成功,一個(gè)回調(diào)預(yù)計(jì)都只會被調(diào)用一次。但有一種特殊情況是,我們可能不知道事件在哪個(gè)時(shí)間點(diǎn)觸發(fā),在這種情況下,EventEmitter是首選。

最后,使用回調(diào)的API僅通知特定的回調(diào),但是使用EventEmitter函數(shù)可以讓多個(gè)監(jiān)聽器都接收到通知。

在定義異步API時(shí),常見的難點(diǎn)是檢查是否使用EventEmitter的事件機(jī)制或僅接受回調(diào)函數(shù)。一般區(qū)分規(guī)則是這樣的:當(dāng)一個(gè)結(jié)果必須以異步方式返回時(shí),應(yīng)該使用回調(diào)函數(shù),當(dāng)需要結(jié)果不確定其方式時(shí),應(yīng)該使用事件機(jī)制來響應(yīng)。

但是,由于這兩者實(shí)在太相近,并且可能兩種方式都能實(shí)現(xiàn)相同的應(yīng)用場景,所以產(chǎn)生了許多混亂。以下列代碼為例:

function helloEvents() {
  const eventEmitter = new EventEmitter();
  setTimeout(() => eventEmitter.emit('hello', 'hello world'), 100);
  return eventEmitter;
}

function helloCallback(callback) {
  setTimeout(() => callback('hello world'), 100);
}

helloEvents()helloCallback()在其功能上可以被認(rèn)為是等價(jià)的,第一個(gè)使用事件機(jī)制實(shí)現(xiàn),第二個(gè)則使用回調(diào)來通知調(diào)用者,而將事件作為參數(shù)傳遞。但是真正區(qū)分它們的是可執(zhí)行性,語義和要實(shí)現(xiàn)或使用的代碼量。雖然我們不能給出一套確定性的規(guī)則來選擇一種風(fēng)格,但我們當(dāng)然可以提供一些提示來幫助你做出決定。

相比于第一個(gè)例子,即觀察者模式而言,回調(diào)函數(shù)在支持不同類型的事件時(shí)有一些限制。但是事實(shí)上,我們?nèi)匀豢梢酝ㄟ^將事件類型作為回調(diào)的參數(shù)傳遞,或者通過接受多個(gè)回調(diào)來區(qū)分多個(gè)事件。然而,這樣做的話不能被認(rèn)為是一個(gè)優(yōu)雅的API。在這種情況下,EventEmitter可以提供更好的接口和更精簡的代碼。

EventEmitter更優(yōu)秀的另一種應(yīng)用場景是多次觸發(fā)同一事件或不觸發(fā)事件的情況。事實(shí)上,無論操作是否成功,一個(gè)回調(diào)預(yù)計(jì)都只會被調(diào)用一次。但有一種特殊情況是,我們可能不知道事件在哪個(gè)時(shí)間點(diǎn)觸發(fā),在這種情況下,EventEmitter是首選。

總結(jié)

最后,使用回調(diào)的API僅通知特定的回調(diào),但是使用EventEmitter函數(shù)可以讓多個(gè)監(jiān)聽器都接收到通知。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容