介紹
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 的值是 undefined
、null
、""
、false
、NaN
等值時,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());
}
}
}