翻譯:瘋狂的技術宅
事件驅動的程式設計變得流行之前,在程式內部進行通訊的標準方法非常簡單:如果一個元件想要向另外一個傳送訊息,只是顯式地呼叫了那個元件上的方法。但是在 react 中用的卻是事件驅動而不是呼叫。
事件的好處
這種方法能夠使元件更加分離。在我們繼續寫程式時,會識別整個過程中的事件,在正確的時間觸發它們,併為每個事件附加一個或多個事件監聽器,這使得**功能擴充套件變得更加容易。**我們可以為特定事件新增更多的 listener,而不必修改現有的偵聽器或觸發事件的應用程式部分。我們所談論的是觀察者模式。
設計一個事件驅動的體系結構
對事件進行識別非常重要,我們不希望最終必須從系統中刪除或替換現有事件,因為這可能會迫使我們刪除或修改附加到事件上的眾多偵聽器。我的一般原則是僅在業務邏輯單元完成執行時才考慮觸發事件。
假如你想在使用者註冊後傳送一堆不同的電子郵件。註冊過程本身可能會涉及許多複雜的步驟和查詢,但從商業角度來看,這只是其中的一個步驟。每個要傳送的電子郵件也是單獨的步驟。因此,一旦註冊完成馬上就釋出事件是很有意義的。於是我們附加了多個監聽器,每個監聽器負責傳送一種型別的電子郵件。
Node的非同步事件驅動架構具有一些被稱為“emitters”的物件。它們發出命名事件,這些事件會呼叫被稱為“listener”的函式。發出事件的所有物件都是 EventEmitter 類的例項。使用它,我們可以建立自己的事件:
一個例子
讓我們使用內建的 events 模組(我建議你檢視這個文件:nodejs.org/api/events.… EventEmitter
的訪問許可權。
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
module.exports = myEmitter;
複製程式碼
這是我們的伺服器端程式的一部分,它負責接收HTTP請求,儲存新使用者併發出事件:
const myEmitter = require('./my_emitter');
// Perform the registration steps
// Pass the new user object as the message passed through by this event.
myEmitter.emit('user-registered', user);
複製程式碼
附加一個監聽器的單獨模組:
const myEmitter = require('./my_emitter');
myEmitter.on('user-registered', (user) => {
// Send an email or whatever.
});
複製程式碼
將策略與實現分開是一種非常好的做法。在這種情況下,策略意味著哪些 listener 訂閱了哪些事件。實現意味著 listener 自己。
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
複製程式碼
module.exports = (user) => {
// Send a welcome email or whatever.
}
複製程式碼
這種分離使 listener 也可以被重複使用,它可以被附加到傳送相同訊息的其他事件上(使用者物件)。同樣重要的是 當多個 listener 被附加到單個事件時,它們將按照附加的順序同步執行。因此 someOtherListener
將在 sendEmailOnRegistration
完成執行後執行。
但是,如果你希望自己的 listener 以非同步方式執行,只需用 setImmediate
包裝它們的實現,如下所示:
module.exports = (user) => {
setImmediate(() => {
// Send a welcome email or whatever.
});
}
複製程式碼
讓你的 Listeners 保持簡潔
在寫 listener 時要堅持單一責任原則。一個 listener 應該只做一件事並把事情做好。例如:要避免在 listener 中編寫太多的條件並根據事件傳來的資料(訊息)去決定做什麼。在這種情況下使用不同的事件會更加合適:
const myEmitter = require('./my_emitter');
// Perform the registration steps
// The application should react differently if the new user has been activated instantly.
if (user.activated) {
myEmitter.emit('user-registered:activated', user);
} else {
myEmitter.emit('user-registered', user);
}
複製程式碼
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
const doSomethingEntirelyDifferent = require('./do_something_entirely_different');
myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
myEmitter.on('user-registered:activated', doSomethingEntirelyDifferent);
view raw
複製程式碼
必要時明確分離 Listener
在前面的例子中,我們的 listener 是完全獨立的函式。但是在 listener 與物件關聯的情況下(這時是一種方法),必須手動將其從已訂閱的事件中分離出來。否則物件將永遠不會被垃圾回收,因為物件( listener )的一部分將會繼續被外部物件( emitter )引用,所以存在記憶體洩漏的可能。
例如,如果我們正在開發一個聊天程式,並且希望當新訊息到達使用者進入的聊天室時,顯示通知的功能應該位於該使用者物件本身的內部,我們可能會這樣做:
class ChatUser {
displayNewMessageNotification(newMessage) {
// Push an alert message or something.
}
// `chatroom` is an instance of EventEmitter.
connectToChatroom(chatroom) {
chatroom.on('message-received', this.displayNewMessageNotification);
}
disconnectFromChatroom(chatroom) {
chatroom.removeListener('message-received', this.displayNewMessageNotification);
}
}
複製程式碼
當使用者關閉他的標籤或暫時斷開網際網路連線時,我們可能希望在伺服器端發起一個回撥,通知其他使用者有人剛剛下線。當然在這時為離線使用者呼叫 displayNewMessageNotification
沒有任何意義。除非我們刪除它,否則它將繼續被用於呼叫新訊息。如果不這樣做,除了不必要的呼叫之外,使用者物件也會被永久地保留在記憶體中。因此在使用者離線時應該在伺服器端回撥中呼叫 disconnectFromChatroom
。
注意事項
如果不小心,即便是鬆散耦合的事件驅動架構也會導致複雜性的增加,可能會導致在系統中跟蹤依賴關係變得很困難。如果我們從偵聽器內部發出事件,程式會特別容易出現這類問題。這可能會觸發意外的事件鏈。