Node.js核心API基於非同步事件驅動的架構,fs.ReadStream可以通過on()方式來監聽事件其實都是由於繼承了EventEmitter類,如下所示
const fs = require('fs');
const EventEmitter = require('events');
var stream = fs.createReadStream('./a.js');
console.log(stream instanceof EventEmitter); // true
複製程式碼
除了流之外,net.Server,以及process也都是繼承自EventEmitter所以可以監聽事件。
const EventEmitter = require('events');
const net = require('net');
var server = net.createServer(function(client) {
console.log(client instanceof EventEmitter); // true
});
server.listen(8000, () => {
console.log('server started on port 8000');
});
console.log(process instanceof EventEmitter); // true
複製程式碼
on監聽的事件的名稱可以包含特殊字元(比如'$'、'*’、'~'都是可以的),但是需要注意是大小寫敏感的。
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('*$~', () => {
console.log('an event occurred!');
});
myEmitter.emit('*$~');
複製程式碼
當EventEmitter物件發出一個事件的時候,所有與此事件繫結的函式都會被同步呼叫。繫結的函式呼叫的返回值都會被忽略掉(這一點會帶來其他問題,後面會提到)。但是如果是物件被修改的話,是可以傳遞到其他監聽函式的,比如:
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', function(data) {
console.log(data.num); // 1
data.num++;
});
myEmitter.on('event', (data) => {
console.log(data.num); // 2
});
myEmitter.emit('event', {
num: 1
});
複製程式碼
這個是JS關於引用型別的特性,與EventEmitter一點關係也沒有,實際情況下不推薦這種寫法,因為可維護性比較低。
如果是自己實現類似EventEmitter機制的話,是可以做到監聽函式之間的執行結果互相傳遞的(比如類似a.pipe(b).pipe(c)這樣,參見之前的釋出訂閱管道化
同步還是非同步
EventEmitter觸發事件的時候,各監聽函式的呼叫是同步的(注意'end'的輸出在最後),但是並不是說監聽函式裡不能包含非同步的程式碼(比如下面的listener2就是一個非同步的)
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
console.log('listener1');
});
myEmitter.on('event', async function() {
console.log('listener2');
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
});
myEmitter.on('event', function() {
console.log('listener3');
});
myEmitter.emit('event');
console.log('end');
// 輸出結果
listener1
listener2
listener3
end
複製程式碼
異常處理
由於監聽函式的執行是同步執行的,所以針對同步的程式碼可以通過try catch捕獲到
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
a.b();
console.log('listener1');
});
myEmitter.on('event', async function() {
console.log('listener2');
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
});
myEmitter.on('event', function() {
console.log('listener3');
});
try {
myEmitter.emit('event');
} catch(e) {
console.error('err');
}
console.log('end');
// 輸出結果
end
err
複製程式碼
但是如果把a.b();移到第二個listener裡面的話就會出現下面的問題
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
console.log('listener1');
});
myEmitter.on('event', async function() {
console.log('listener2');
a.b();
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
});
myEmitter.on('event', function() {
console.log('listener3');
});
try {
myEmitter.emit('event');
} catch(e) {
console.error('err');
}
console.log('end');
// 輸出結果
listener1
listener2
listener3
end
(node:9046) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): ReferenceError: a is not defined
(node:9046) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code
複製程式碼
async函式的特點就在於它的返回值是一個Promise,如果函式體內出現錯誤的話Promise就是reject狀態。Node.js不推薦忽略reject的promise,而EventEmitter對於各監聽函式的返回值是忽略的,所以才會出現上面的情況。明白了問題的原因後我們就可以確定對於上面的情況的話,需要在第二個listener裡面增加try catch的處理。
當事件被觸發時,如果沒有與該事件繫結的函式的話,該事件會被靜默忽略掉,但是如果事件的名稱是error的話,沒有與此相關的事件處理的話,程式就會crash退出
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', function(data) {
console.log(data);
});
myEmitter.emit('error');
events.js:199
throw err;
^
Error [ERR_UNHANDLED_ERROR]: Unhandled error.
at MyEmitter.emit (events.js:197:19)
at Object.<anonymous> (/Users/xiji/workspace/learn/event-emitter/b.js:7:11)
at Module._compile (module.js:641:30)
at Object.Module._extensions..js (module.js:652:10)
at Module.load (module.js:560:32)
at tryModuleLoad (module.js:503:12)
at Function.Module._load (module.js:495:3)
at Function.Module.runMain (module.js:682:10)
at startup (bootstrap_node.js:191:16)
at bootstrap_node.js:613:3
複製程式碼
只有新增了針對error事件的處理函式的話程式才不會退出了。
另外一種方式是process監聽uncaughtException事件,但是這並不是推薦的做法,因為uncaughtException事件是非常嚴重的,通常情況下在uncaughtException的處理函式裡面一般是做一些上報或者清理工作,然後執行process.exit(1)讓程式退出了。
process.on('uncaughtException', function(err) {
console.error('uncaught exception:', err.stack || err);
// orderly close server, resources, etc.
closeEverything(function(err) {
if (err)
console.error('Error while closing everything:', err.stack || err);
// exit anyway
process.exit(1);
});
});
複製程式碼
監聽一次
如果在同一時刻出現了多次uncaught exception的話,那麼closeEverything就可能會被觸發多次,這又有可能會帶來新的問題。因此推薦的做法是隻對第一次的uncaught excepition做監聽處理,這種情況下就需要用到once方法了
process.once('uncaughtException', function(err) {
// orderly close server, resources, etc.
closeEverything(function(err) {
if (err)
console.error('Error while closing everything:', err.stack || err);
// exit anyway
process.exit(1);
});
});
複製程式碼
按照上面的寫法就不會出現closeEverything被觸發兩次的現象了,不過對於第二次的uncaughtException因為沒有相應的處理函式,會導致程式立即退出,為了解決這個問題,我們可以在once之外,再增加每次異常的錯誤記錄,如下所示:
process.on('uncaughtException', function(err) {
console.error('uncaught exception:', err.stack || err);
});
複製程式碼
監聽函式的執行順序
之前的例子(on(eventName, listener))可以看到各監聽函式的執行順序與程式碼的抒寫順序一致,EventEmitter還提供了其他的方法可以調整監聽函式的執行順序,雖然並不如釋出訂閱管道化那樣靈活。
除了on的方式(向後追加),我們還可以使用prependListener的方法來(向前插入)增加監聽函式
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.prependListener('event', function() {
console.log('listener1');
});
myEmitter.prependListener('event', async function() {
console.log('listener2');
});
myEmitter.prependListener('event', function() {
console.log('listener3');
});
myEmitter.emit('event');
console.log('end');
// 輸出結果
listener3
listener2
listener1
end
複製程式碼
EventEmiter在每次有新的listener加入之前都會觸發一個'newListener'的事件,所以可以也可以通過監聽這個事件來實現向前插入監聽函式,但是需要注意的一點是為了避免無限迴圈的出現,如果在newListener的監聽函式裡有增加監聽函式的程式碼的話,那麼對於newListener的監聽應該使用once方式。
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.once('newListener', (event, listener) => {
if (event === 'event') {
myEmitter.on('event', () => {
console.log('B');
});
}
});
myEmitter.on('event', () => {
console.log('A');
});
myEmitter.emit('event');
// 輸出結果
// B
// A
複製程式碼
調整預設最大listener
預設情況下針對單一事件的最大listener數量是10,如果超過10個的話listener還是會執行,只是控制檯會有警告資訊,告警資訊裡面已經提示了操作建議,可以通過呼叫emitter.setMaxListeners()來調整最大listener的限制
(node:9379) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit
複製程式碼
上面的警告資訊的粒度不夠,並不能告訴我們是哪裡的程式碼出了問題,可以通過process.on('warning')來獲得更具體的資訊(emitter、event、eventCount)
process.on('warning', (e) => {
console.log(e);
})
{ MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit
at _addListener (events.js:289:19)
at MyEmitter.prependListener (events.js:313:14)
at Object.<anonymous> (/Users/xiji/workspace/learn/event-emitter/b.js:34:11)
at Module._compile (module.js:641:30)
at Object.Module._extensions..js (module.js:652:10)
at Module.load (module.js:560:32)
at tryModuleLoad (module.js:503:12)
at Function.Module._load (module.js:495:3)
at Function.Module.runMain (module.js:682:10)
at startup (bootstrap_node.js:191:16)
name: 'MaxListenersExceededWarning',
emitter:
MyEmitter {
domain: null,
_events: { event: [Array] },
_eventsCount: 1,
_maxListeners: undefined },
type: 'event',
count: 11 }
複製程式碼
this指向
監聽函式如果採用如下寫法的話,那麼this的指向就是事件的emitter
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', function(a, b) {
console.log(a, b, this === myEmitter); // a b true
});
myEmitter.emit('event', 'a', 'b');
複製程式碼
如果是用箭頭函式寫法的話,那麼this就不是指向emitter了
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
console.log(a, b, this === myEmitter); // a b false
});
myEmitter.emit('event', 'a', 'b');
複製程式碼
其他
emitter.off(eventName, listener) 、emitter.removeListener(eventName, listener)、emitter.removeAllListeners([eventName])可以移除監聽。函式的返回值是emitter物件,因此可以使用鏈式語法
emitter.listenerCount(eventName)可以獲取事件註冊的listener個數
emitter.listeners(eventName)可以獲取事件註冊的listener陣列副本。
參考資料
https://netbasal.com/javascript-the-magic-behind-event-emitter-cce3abcbcef9
https://medium.com/technoetics/node-js-event-emitter-explained-d4f7fd141a1a
https://medium.com/yld-engineering-blog/using-an-event-emitter-common-use-and-edge-cases-b5eb518a4bd2
https://medium.freecodecamp.org/understanding-node-js-event-driven-architecture-223292fcbc2d
https://nodejs.org/api/events.html