一惜论、背景
先看一個DOM
事件:
const button = document.querySelector("button");
button.addEventListener("click", (event) => /* do something with the event */)
以上代碼在button
上添加了一個事件監(jiān)聽器,每當點擊button
的時候止喷,點擊事件被觸發(fā)出去并且同時調用callback
函數馆类。
有很多時候可能會有需要觸發(fā)自定義事件的需求,不單單只是一個點擊事件弹谁,假設有這樣一個需要觸發(fā)一個基于其他觸發(fā)器的事件乾巧,并且需要有一個事件響應的句喜,可以自定義一個event emitter
來實現。
一個event emitter
就是監(jiān)聽一個event
沟于,觸發(fā)一個回調函數咳胃,然后emit
一個帶有value
的事件的一種模式,有時候也稱為pub/sub
模型或者監(jiān)聽器社裆。
在JavaScript中的一種實現如下:
let n = 0;
const event = new EventEmitter();
event.subscribe("THUNDER_ON_THE_MOUNTAIN", value => (n = value));
event.emit("THUNDER_ON_THE_MOUNTAIN", 18);
// n: 18
event.emit("THUNDER_ON_THE_MOUNTAIN", 5);
// n: 5
在上面的代碼中拙绊,我們訂閱了一個叫做 THUNDER_ON_THE_MOUNTAIN
的事件,并且當事件被 emitted
的時候泳秀,回調函數 value => (n = value)
也會被觸發(fā)标沪,可以調用 emit()
來emit
該事件。
這在與異步代碼交互的時候嗜傅,如果有不在當前模塊下的值需要更新時十分有用金句。
一個真實的例子就是React Redux,
Redux
需要一種通知外部其內部的值已經更新的機制吕嘀,其允許React
調用setState()
并重新渲染UI來獲取哪些值已經改變违寞,這個地方也是使用event emitter
來實現的。
Redux store
有一個傳入一個提供新的store
的回調函數作為參數的訂閱函數偶房,在這個訂閱函數中趁曼,調用了 React Redux
的以新store
的值調用了setState()
方法的 <Provider>
組件,可以在此查看棕洋。
現在我們的應用有了兩個不同的部分挡闰,一部分是React UI
,另一部分是Redux store
掰盘,誰也說不清楚事件究竟是被那一部分觸發(fā)的摄悯。
二、實現
先看一個簡單的event emitter
愧捕,其中使用了class
奢驯,在這個class
中跟蹤事件。
class EventEmitter {
public events: Events;
constructor(events?: Events) {
this.events = events || {};
}
}
- 事件
定義一個事件接口次绘,用來存儲一個每一個key
都是一個事件名并且各自的值都是回調函數組成的數組的空白對象瘪阁。
interface Events {
[key: string]: Function[];
}
/**
{
"event": [fn],
"event_two": [fn]
}
*/
使用數組的原因是因為每一個事件都可能有多個subscriber
,因為element.addEventLister("click")
可能會被多次調用邮偎。
- 訂閱
現在需要處理訂閱的事件罗洗,在上面的例子中,subscribe()
函數接收兩個參數:一個name
和一個callback
函數钢猛。
event.subscribe("named event", value => value);
定義一個subscribe
方法來接收這兩個參數,只需把這兩個參數添加到類內部的this.events
轩缤。
class EventEmitter {
public events: Events;
constructor(events?: Events) {
this.events = events || {};
}
public subscribe(name: string, cb: Function) {
(this.events[name] || (this.events[name] = [])).push(cb);
}
}
- 發(fā)射
到此可以訂閱事件了命迈,接下來贩绕,當一個新事件發(fā)射的時候需要觸發(fā)回調函數,當觸發(fā)的時候壶愤,將使用(emit("event"))
中存儲的事件名和需要傳遞到回調函數(emit("event", value))
的任意值淑倾,我們可以簡單地傳遞任意參數到回調函數在第一個參數后面。
class EventEmitter {
public events: Events;
constructor(events?: Events) {
this.events = events || {};
}
public subscribe(name: string, cb: Function) {
(this.events[name] || (this.events[name] = [])).push(cb);
}
public emit(name: string, ...args: any[]): void {
(this.events[name] || []).forEach(fn => fn(...args));
}
}
既然我們知道了我們希望發(fā)射的事件征椒,可以使用this.events[name]
來查看娇哆,返回的是一個回調函數的數組。
- 取消訂閱
subscribe(name: string, cb: Function) {
(this.events[name] || (this.events[name] = [])).push(cb);
return {
unsubscribe: () =>
this.events[name] && this.events[name].splice(this.events[name].indexOf(cb) >>> 0, 1)
};
}
上述代碼返回一個帶有unsubscribe
方法的對象勃救,可以使用箭頭函數() =>
來獲取傳遞給父對象參數的作用域碍讨,在這個函數中,使用>>>
操作符可以找到傳遞給父級回調函數的索引蒙秒,在這里使用可以保證我們每次在回調函數數組上調用splice()
的時候總是可以取到一個真正的數字勃黍,即使indexOf()
都不能返回數字也行。
可以這樣使用:
const subscription = event.subscribe("event", value => value);
subscription.unsubscribe();
到此晕讲,我們就可以取消這一個特別的訂閱了覆获,而且不影響其他的訂閱。
- 完整實現
interface Events {
[key: string]: Function[];
}
export class EventEmitter {
public events: Events;
constructor(events?: Events) {
this.events = events || {};
}
public subscribe(name: string, cb: Function) {
(this.events[name] || (this.events[name] = [])).push(cb);
return {
unsubscribe: () =>
this.events[name] && this.events[name].splice(this.events[name].indexOf(cb) >>> 0, 1)
};
}
public emit(name: string, ...args: any[]): void {
(this.events[name] || []).forEach(fn => fn(...args));
}
}
上述代碼中瓢省,首先在另外一個事件回調中使用了event emitter
弄息,在這種情況下,一個event emitter
是用來清除一些邏輯勤婚,在GitHub上選擇一個倉庫摹量,獲取詳情,緩存詳情蛔六,并更新DOM去顯示這些詳情荆永。在訂閱回調函數中從網絡或者緩存中獲取結果并更新,可以這樣做的原因是當我們發(fā)射時間的時候從列表中給了回調函數一個隨機的倉庫国章。
現在來考慮一些不太一樣的東西具钥,在一個應用中,可能會有許多狀態(tài)需要登錄之后才可以觸發(fā)液兽,并且可能會有多個訂閱器來處理用戶試圖退出的操作骂删。因為已經發(fā)射了一個帶false
值的事件,每一個訂閱器都可以使用這個值四啰,并且需要判斷是否需要重定向頁面宁玫,移除cookie或者禁用表單。
const events = new EventEmitter();
events.emit("authentication", false);
events.subscribe("authentication", isLoggedIn => {
buttonEl.setAttribute("disabled", !isLogged);
});
events.subscribe("authentication", isLoggedIn => {
window.location.replace(!isLoggedIn ? "/login" : "");
});
events.subscribe("authentication", isLoggedIn => {
!isLoggedIn && cookies.remove("auth_token");
});
- 最后
要讓emitters
能工作柑晒,有幾點需要考慮: - 需要在
emit()
函數中使用forEach
或者map
來確保我們能創(chuàng)建新的訂閱器或者取消訂閱欧瘪。 - 當一個
EventEmitter
類被實例化之后,可以傳遞一個預定義的事件到事件接口匙赞。 - 可以不需要使用
class
佛掖,來實現妖碉,個人喜好,但是使用class
使事件存儲在哪里會更加清晰芥被。
可以在一個函數中實現欧宜,如下:
function emitter(e?: Events) {
let events: Events = e || {};
return {
events,
subscribe: (name: string, cb: Function) => {
(events[name] || (events[name] = [])).push(cb);
return {
unsubscribe: () => {
events[name] && events[name].splice(events[name].indexOf(cb) >>> 0, 1);
}
};
},
emit: (name: string, ...args: any[]) => {
(events[name] || []).forEach(fn => fn(...args));
}
};
}