短小精悍的釋出訂閱庫 mitt

qinghuanI發表於2023-02-14

介紹

mitt 是一個小而美的釋出-訂閱庫,短短的幾十行程式碼,小於 200b 的體積,提供三個重要的 API。然而麻雀雖小,五臟俱全。

釋出-訂閱模式

釋出-訂閱模式定義了一種一對多的依賴關係,讓多個訂閱者物件同時監聽某一個主題物件。這個主題物件在自身狀態變化時,會通知所有訂閱者物件,使它們能夠自動更新自己的狀態。

用法

有這樣的一個需求,小明想買 100w 以內的二手房,小華想買 150w 左右的二手房,但是房產中介告訴他倆,暫時沒有他們想要的房源,中介的工作人員留下他倆的聯絡方式,一旦有合適的房源就透過電話通知。這就是一個經典使用釋出-訂閱的模式的場景。請看下面的程式碼。

const mitt = require("mitt");

const houseAgents = mitt();

houseAgents.on("xiaoming", () => {
  console.log("有 100w 以內的房源了");
});

houseAgents.on("xiaohua", () => {
  console.log("有 150w 左右的房源了");
});

過了一段時間,中介收到 150w 左右的房源,立馬通知 xiaohua。

houseAgents.emit("xiaohua");

原始碼分析

mitt 透過幾十行程式碼實現了釋出-訂閱機制。我們來剖析一下 mitt 原始碼

function mitt(all) {
  all = all || Object.create(null);

  return {
    on: function () {
      /* some code*/
    },
    emit: function () {
      /* some code*/
    },
    off: function () {
      /* some code*/
    },
  };
}

mitt 庫的原始碼中只有一個 mitt 函式。它接收 all 引數,返回一個物件,該物件包含 on、emit、off 三個方法。

all = all || Object.create(null);

mitt 方法接收 all 引數,all 用來儲存監聽的事件。當傳入的 all 的值是 undefinednull""falseNaN 等值時,all 的值預設為 Object.create(null), 它是一個沒有 __proto__ 屬性的物件。

實際上,這裡缺少型別處理。比如 mitt(true), 就會導致錯誤。

const mitt = require("mitt");
const ob = mitt(true);

ob.on("a", () => {});
ob.emit("a", "dd");

建議 all 的型別是物件型別或者不傳。推薦傳入空物件或陣列。

const mitt = require("mitt");
const ob = mitt();
/* 
  or
  const ob = mitt([]);
  const ob = mitt({});

*/

接下來,我們來看看 mitt 裡非常重要的三個方法。

// ...
on: function on(type, handler) {
  (all[type] || (all[type] = [])).push(handler);
}

// ...

on 接收兩個引數,type 型別和 handler 事件處理方法。方法體內僅有一行非常優雅的程式碼。當該型別存在時,就將其事件處理追加到陣列後面,當該型別不存在時,初始化一個空陣列,用來儲存該型別的事件處理方法。

emit: function emit(type, evt) {
  (all[type] || []).slice().map(function (handler) {
    handler(evt);
  });
  (all["*"] || []).slice().map(function (handler) {
    handler(type, evt);
  });
}

訂閱事件使用 on 方法,釋出事件使用 emit 方法。emit 方法裡就做了一件事,根據事件型別,將該型別訂閱的所有事件遍歷呼叫。

不足

透過分析 mitt 的原始碼,我們會發現,mitt 沒有考慮匿名函式情況,在使用 on 方法時,傳入的第二個引數必須是具名函式。

手動實現釋出訂閱庫

class PubSub {
  constructor() {
    this.listeners = [];
  }

  sub(type, handler, always) {
    console.log(this.listeners[type] || []);
    if (!this.listeners[type]) {
      this.listeners[type] = [];
    }
    this.listeners[type].push({ handler, always });
  }

  on(type, handler, always = true) {
    this.sub(type, handler, always);
  }

  once(type, handler, always = false) {
    this.sub(type, handler, always);
  }

  emit(type, evt) {
    if (this.listeners[type]) {
      this.listeners[type].forEach((listener) => {
        listener.handler(evt);
      });

      this.listeners[type] = this.listeners[type].slice().filter((listener) => Boolean(listener.always));
    }
  }

  off(type, handler) {
    if (this.listeners[type]) {
      this.listeners[type] = this.listeners[type]
        .slice()
        .filter((listener) => listener.handler.toString() !== handler.toString());
    }
  }
}

相關文章