在Node中锦爵,要實現(xiàn)觀察者模式非常的簡單,而且內(nèi)置于EventEmitter
類中宛篇,EventEmitter
類允許我們注冊一個或者多個函數(shù)作為監(jiān)聽者计福,當(dāng)對應(yīng)的事件觸發(fā)后跌捆,它們就會被觸發(fā)
EventEmitter
是一個原型,可以通過events這個核心模塊獲取得到
const EventEmitter = require('events').EventEmitter;
const eventEmitter = new EventEmitter();
EventEmitter
內(nèi)部提供了幾個API
-
on(event, listener)
注冊監(jiān)聽者 -
once(event, listener)
注冊監(jiān)聽者象颖,但是只會觸發(fā)一次 -
emit(event, [arg1], [...])
發(fā)布一個事件 -
removeListener(event, listener)
移除監(jiān)聽者
上面的方法都支持鏈?zhǔn)秸{(diào)用佩厚。而且我們會發(fā)現(xiàn)listener和我們所知道的傳統(tǒng)的Node回調(diào)函數(shù)是不同的,最突出的就是函數(shù)的第一個參數(shù)不是error说订,而是我們emit時候穿過去的參數(shù)
使用EventEmitter
使用它最簡單的方式就是去創(chuàng)建一個新的實例抄瓦,然后立刻使用它
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');
})
傳播錯誤
EventEmitter
和回調(diào)函數(shù)一樣,當(dāng)異步事件發(fā)生錯誤的時候陶冷,直接拋出異常是無法被捕捉到的钙姊,因為它們所處的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');
})
所以我們平時在寫代碼的時候,要注意埂伦,多增加一個關(guān)于error
的事件進(jìn)行監(jiān)聽煞额,然后進(jìn)一步處理
使任何對象可觀察
有些時候,我們直接通過EventEmitter
去創(chuàng)建一個可觀察對象是不夠的沾谜,因為它不能夠為我們提供擴展的功能膊毁,所以更多的時候,我們通過繼承EventEmitter
這個類类早,去創(chuàng)建一個更具有擴展性的可觀察的對象
為了進(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ā)的逆日,但是強調(diào)的是嵌巷,在同一個EventEmitter
內(nèi),不能混用兩種模式
emit同步事件和異步事件最主要的區(qū)別取決于注冊listener的方法室抽,如果events是異步的emit搪哪,那么程序有充足的時間去注冊listener,因為event不會在本次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);
})
上面的代碼晓折,就是因為沒有在emit之前進(jìn)行注冊,導(dǎo)致沒有任何的輸出結(jié)果兽泄,如果是異步的去emit的話漓概,就不會這樣。
所以我們在使用EventEmitter
的時候病梢,需要考慮好使用的場景胃珍,再根據(jù)場景決定使用同步的方式還是異步的方式
EventEmitter VS callbacks
在定義異步API
時,常見的難點是檢查是否使用EventEmitter
的事件機制或僅接受回調(diào)函數(shù)蜓陌。一般區(qū)分規(guī)則是這樣的:當(dāng)一個結(jié)果必須以異步方式返回時觅彰,應(yīng)該使用回調(diào)函數(shù),當(dāng)需要結(jié)果不確定其方式時钮热,應(yīng)該使用事件機制來響應(yīng)填抬。
但是,由于這兩者實在太相近隧期,并且可能兩種方式都能實現(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)為是等價的厌秒,第一個使用事件機制實現(xiàn)读拆,第二個則使用回調(diào)來通知調(diào)用者,而將事件作為參數(shù)傳遞鸵闪。但是真正區(qū)分它們的是可執(zhí)行性檐晕,語義和要實現(xiàn)或使用的代碼量。雖然我們不能給出一套確定性的規(guī)則來選擇一種風(fēng)格蚌讼,但我們當(dāng)然可以提供一些提示來幫助你做出決定辟灰。
相比于第一個例子篡石,即觀察者模式而言芥喇,回調(diào)函數(shù)在支持不同類型的事件時有一些限制凰萨。但是事實上械馆,我們?nèi)匀豢梢酝ㄟ^將事件類型作為回調(diào)的參數(shù)傳遞,或者通過接受多個回調(diào)來區(qū)分多個事件武通。然而霹崎,這樣做的話不能被認(rèn)為是一個優(yōu)雅的API
冶忱。在這種情況下,EventEmitter
可以提供更好的接口和更精簡的代碼囚枪。
EventEmitter
更優(yōu)秀的另一種應(yīng)用場景是多次觸發(fā)同一事件或不觸發(fā)事件的情況派诬。事實上,無論操作是否成功链沼,一個回調(diào)預(yù)計都只會被調(diào)用一次千埃。但有一種特殊情況是忆植,我們可能不知道事件在哪個時間點觸發(fā),在這種情況下朝刊,EventEmitter
是首選拾氓。
最后冯挎,使用回調(diào)的API
僅通知特定的回調(diào),但是使用EventEmitter
函數(shù)可以讓多個監(jiān)聽器都接收到通知房官。
在定義異步API
時续滋,常見的難點是檢查是否使用EventEmitter
的事件機制或僅接受回調(diào)函數(shù)。一般區(qū)分規(guī)則是這樣的:當(dāng)一個結(jié)果必須以異步方式返回時疲酌,應(yīng)該使用回調(diào)函數(shù),當(dāng)需要結(jié)果不確定其方式時湿颅,應(yīng)該使用事件機制來響應(yīng)粥诫。
但是,由于這兩者實在太相近怀浆,并且可能兩種方式都能實現(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)為是等價的搀玖,第一個使用事件機制實現(xiàn),第二個則使用回調(diào)來通知調(diào)用者芳来,而將事件作為參數(shù)傳遞猜拾。但是真正區(qū)分它們的是可執(zhí)行性,語義和要實現(xiàn)或使用的代碼量挎袜。雖然我們不能給出一套確定性的規(guī)則來選擇一種風(fēng)格,但我們當(dāng)然可以提供一些提示來幫助你做出決定紊搪。
相比于第一個例子全景,即觀察者模式而言,回調(diào)函數(shù)在支持不同類型的事件時有一些限制爸黄。但是事實上,我們?nèi)匀豢梢酝ㄟ^將事件類型作為回調(diào)的參數(shù)傳遞梆奈,或者通過接受多個回調(diào)來區(qū)分多個事件称开。然而,這樣做的話不能被認(rèn)為是一個優(yōu)雅的API
径荔。在這種情況下脆霎,EventEmitter
可以提供更好的接口和更精簡的代碼。
EventEmitter
更優(yōu)秀的另一種應(yīng)用場景是多次觸發(fā)同一事件或不觸發(fā)事件的情況睛蛛。事實上胧谈,無論操作是否成功荸频,一個回調(diào)預(yù)計都只會被調(diào)用一次。但有一種特殊情況是稳强,我們可能不知道事件在哪個時間點觸發(fā)和悦,在這種情況下,EventEmitter
是首選褒繁。
總結(jié)
最后馍忽,使用回調(diào)的API
僅通知特定的回調(diào),但是使用EventEmitter
函數(shù)可以讓多個監(jiān)聽器都接收到通知遭笋。