深入理解JS中的事件發(fā)射器(Event Emitters)

Event Emitter

一惜论、背景

先看一個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));
    }
  };
}

參考

https://css-tricks.com/understanding-event-emitters/

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拴魄,隨后出現的幾起案子冗茸,更是在濱河造成了極大的恐慌,老刑警劉巖匹中,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件夏漱,死亡現場離奇詭異,居然都是意外死亡职员,警方通過查閱死者的電腦和手機麻蹋,發(fā)現死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來焊切,“玉大人扮授,你說我怎么就攤上這事∽ǚ荆” “怎么了刹勃?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嚎尤。 經常有香客問我荔仁,道長,這世上最難降的妖魔是什么芽死? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任乏梁,我火速辦了婚禮,結果婚禮上关贵,老公的妹妹穿的比我還像新娘遇骑。我一直安慰自己,他們只是感情好揖曾,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布落萎。 她就那樣靜靜地躺著,像睡著了一般炭剪。 火紅的嫁衣襯著肌膚如雪练链。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天奴拦,我揣著相機與錄音媒鼓,去河邊找鬼。 笑死,一個胖子當著我的面吹牛绿鸣,可吹牛的內容都是我干的瓷产。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼枚驻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了株旷?” 一聲冷哼從身側響起再登,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎晾剖,沒想到半個月后锉矢,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡齿尽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年沽损,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片循头。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡绵估,死狀恐怖,靈堂內的尸體忽然破棺而出卡骂,到底是詐尸還是另有隱情国裳,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布全跨,位于F島的核電站缝左,受9級特大地震影響,放射性物質發(fā)生泄漏浓若。R本人自食惡果不足惜渺杉,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挪钓。 院中可真熱鬧是越,春花似錦、人聲如沸诵原。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绍赛。三九已至蔓纠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吗蚌,已是汗流浹背腿倚。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蚯妇,地道東北人敷燎。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓暂筝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親硬贯。 傳聞我的和親對象是個殘疾皇子焕襟,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內容