title: 微型庫解讀之200byte的EventEmitter - Mitt
tags: 前端
date: 2018-01-26 00:55:08
起源
關於 EventEmitter
我想應該很多同學都很熟悉了。簡而言之是一個事件的釋出與訂閱器。
這兩天讀到了一些非常有意思的小庫,雖然小但是功能完備,比如說這次我們要講解的 Mitt.
小
Mitt
是一個微型的 EventEmitter
庫,實現了基本的 on
, off
, emit
三個Api,對於使用 EventEmitter 其他功能不多的同學來說,200byte 的體積可以說是非常划算了。
當然小也有其付出的代價,那就是隻支援這三個功能。
至於怎麼取捨,見仁見智吧,我建議是先使用 mitt
,就算後期要更換別的庫,因為 Api 統一,所以更換起來基本不費事。
Mitt
在 Github的 demo 中,也顯示出了程式碼雖小,五臟俱全的特點。
Demo:
import mitt from 'mitt'
let emitter = mitt()
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// listen to all events
emitter.on('*', (type, e) => console.log(type, e) )
// fire an event
emitter.emit('foo', { a: 'b' })
// working with handler references:
function onFoo() {}
emitter.on('foo', onFoo) // listen
emitter.off('foo', onFoo) // unlisten
程式碼解讀
在研究 Mitt
能完成的功能後,也在想為什麼能做到這麼小。
在這兒對一些閃光點做一些解讀。
節約記憶體且避免衝突的 EventMap
export default function mitt(all: EventHandlerMap) {
all = all || Object.create(null);
return {
// ...Api
}
}
在初始化 mitt 時,會有一個可選的引數 all
,用於存放要監聽的事件。
如果初始化不傳參時,會使用 Object.create(null)
來實現。
這樣的好處在於,生成的物件是一個原型為空的物件。
優點如下:
- 節約記憶體
- 避免衝突
節約記憶體是因為沒有了原型,可以節省部分開銷。
避免衝突則是因為在普通物件中,當要觸發的事件與物件原型上的屬性或方法重名時,會出現事件不存在卻被錯誤觸發導致不必要的問題。
var obj = {};
console.log('toString' in obj);
var noPrototypeObj = Object.create(null);
console.log('toString' in noPrototypeObj);
輸出結果如下:
簡潔的佇列初始化
經常我們會做這樣一個操作,當物件中某個屬性不存在時,就初始化,存在則直接返回值。用程式碼表示就是:
var obj = {};
var getQueue = (key) => {
if (!obj[key]) {
obj[key] = []
}
return obj[key]
}
這是一個很常見的操作,但是在 mitt
的卻簡潔了很多。
export default function mitt(all: EventHandlerMap) {
all = all || Object.create(null);
return {
/**
* Register an event handler for the given type.
*
* @param {String} type Type of event to listen for, or `"*"` for all events
* @param {Function} handler Function to call in response to given event
* @memberOf mitt
*/
on(type: string, handler: EventHandler) {
(all[type] || (all[type] = [])).push(handler);
}
};
}
在 on
函式之中,有這麼一句:(all[type] || (all[type] = []))
這個表示式的意思很簡單,有值取值,無值初始化。
但是總的程式碼量比起之前的程式碼小了很多,實現了簡化程式碼的目的。
PS:這個操作我之前在讀 React setState 原始碼時,也碰到過。
其中 queue 的獲取便是使用了這種方式。
無符號右移(>>>)
在 off
的Api中,有使用到無符號右移(>>>)的操作,具體操作如下:
/**
* Remove an event handler for the given type.
*
* @param {String} type Type of event to unregister `handler` from, or `"*"`
* @param {Function} handler Handler function to remove
* @memberOf mitt
*/
off(type: string, handler: EventHandler) {
if (all[type]) {
all[type].splice(all[type].indexOf(handler) >>> 0, 1);
}
}
其中 all[type].splice(all[type].indexOf(handler) >>> 0, 1);
這一句的作用可謂亮眼。
移除某個指定的事件監聽是很正常的事,但可能會有一個問題,就是傳入的要移除的監聽器並不存在。
換做以往的程式碼,可能你會先搜尋,再決定是否執行移除操作,但是這樣一來程式碼量就又增加了。
而無符號右移(>>>),恰恰符合我們的需要。
具體的作用如 demo,在搜尋的事件監聽函式不存在時,會返回一個極大的正數,傳入 splice 後,並不會刪除已有的函式監聽器,從而實現了想要的功能。
監聽所有事件的監聽器
在日常開發中,經常可能想監聽所有的事件,來輔助開發。
而 mitt
就實現了這個功能。
Demo:
import mitt from 'mitt'
let emitter = mitt()
// listen to all events
emitter.on('*', (type, e) => console.log(type, e) )
而在原始碼裡,這個的實現很簡潔:
/**
* Invoke all handlers for the given type.
* If present, `"*"` handlers are invoked after type-matched handlers.
*
* @param {String} type The event type to invoke
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
* @memberOf mitt
*/
emit(type: string, evt: any) {
(all[type] || []).slice().map((handler) => { handler(evt); });
(all['*'] || []).slice().map((handler) => { handler(type, evt); });
}
就是在使用 emit
函式時,找出事件型別為 *
的監聽器,並觸發它。
小而美
Mitt
的整個庫非常的小,但是卻功能齊全,為了縮減程式碼,也是有一些小技巧在裡面。
但是 Mitt
的庫小也有缺點,比如引數的型別如果傳錯了,它並不會預先提示你,這也算是一個要取捨的點吧。
這幾天也在瘋狂的看 developit 寫的一些庫,他的庫都有小而美的特點,無論是著名的 Preact
還是簡單的 mitt
這種庫。他寫的程式碼,還是挺值得一讀的。
計劃
之後的計劃,可能也準備寫幾篇這種微型庫的原始碼閱讀文章,這種庫讀起來輕鬆,適合每天讀一兩個,而能學到的東西和思路也不少。