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

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)聽器都接收到通知。