前情提要 —— 一種常見的Event物件的實現:
function initEventMap(instance, eventName) {
const { enents } = instance;
events[eventName] = events[eventName] || [];
return events[eventName];
}
class FEvent {
constructor() {
this.events = {}; // 儲存繫結的事件
}
$on(eventName, handle) {
initEventMap(this.events, eventName).push(handle);
}
$emit(eventName, data) {
initEventMap(this.events, eventName).forEach(handle => handle(data));
}
$off(eventName) {}
}
複製程式碼
使用方式分析
全域性方式
[global].event = new FEvent();
//or in event module file
export new FEvent();
複製程式碼
歷史上我幹過不少這種事,比如在Vue框架下,以export new Vue()
的方式作為事件模組使用,這樣所有事件都指向同一個事件例項。一般倒也沒什麼問題,但是中大型應用中事件的管理可能會有點麻煩:
- 需要留意 eventName 的定義:為了避免重名可能通常會需要加上一些字首,eg:
event.$on('article-update', doSth)
event.$on('article-tag-update', doSth)
event.$on('article-title-update', doSth)
複製程式碼
-
關注事件解綁:事件例項是一個全域性的例項,被繫結的事件及其引用的上下文環境,在不解綁的情況下,將不能被釋放,這可能會引起記憶體洩漏。例子參考:記一次網頁記憶體溢位分析及解決實踐
不過以上兩點,總是需要人為去注意。多人協作、或者個人狀態不好,難免出現差錯。 複製程式碼
與例項繫結
DOM、VueComponent 等,都是這種方式
// extends 方式
class Article extends FEvent {
constructor{
this.events = {}; // 用於儲存例項上的事件
}
}
// mixin 方式
function mixinEvent(target) {
const protos = FEvent.prototype;
Object.getOwnPropertyNames(protos).forEach((key) => {
if (key !== 'constructor') {
target.prototype[key] = protos[key];
}
});
target.prototype.events = {};
}
class Article {}
mixinEvent(Article);
// decorator 方式
@mixinEvent // 實現同 mixin
class A {}
// use
const article = new A();
article.$on(...);
article.$emit(...);
複製程式碼
除了class 定義上麻煩一點,但是避免了全域性事件方式的一些困擾:
- 一般不用擔心事件繫結帶來的記憶體洩漏問題,只要例項不被引用,不用擔心繫結的事件會駐留記憶體
- 不用擔心事件重名問題
- 特定場景下減少事件引數的判斷,比如以下場景:
event.$on('article-update', (article) => {
if (article === this.currentArticle) {
// doSth
}
});
複製程式碼
在實際使用時遇到的一些困擾以及處理
extens/mixin 時需要宣告 events 屬性
之前期望借用 Vue 的 event 功能,程式碼模板長這樣:
class A extends Vue {
constructor(){
this._events = {}; // 這裡就有點彆扭了,畢竟不是公開介面
}
}
複製程式碼
但是這不是標準用法,_events 屬性還是看了原始碼才知道的。
所以,對於 Event 的實現,需要做一些調整:
function initEventMap(instance, eventName) {
// Step1: 在使用時進行檢查並初始化
if (!instance.events) {
instance.events = {}; // 或者利用 WeakMap私有化 events
}
const { enents } = instance;
events[eventName] = events[eventName] || [];
return events[eventName];
}
// Step2: 移除 FEvent 的 constructor
複製程式碼
事件的 Promise 化
在某些場景下,可能會期望 $emit 之後,拿到被觸發函式的執行結果。比如在 Vue 中有這樣的巢狀模板:
<template>
<compA>
<compB :event="myEvent">
</compA>
</template>
<script>
export default {
data() {
myEvent: new FEvent(),
}
}
</script>
複製程式碼
期望利用 event 的方式代替 ref 呼叫 compB 中方法,並得到該方法的執行結果。於是可以有這樣的方式:
// in compB
this.event.$on('compBMethod', async ({ data, callback }) => {
const res = await this.compBMethod(data);
callback(res);
});
// use
new Promise((resolve) => {
ins.$emit('compBMethod', { data, resolve });
}).then(doSth);
複製程式碼
或者,給 FEvent 擴充套件一個例項方法:
{
$promiseEmit(eventName, ...args) {
const events = initEventMap(this, eventName);
const defers = events.map(
async handler => await handler.apply(this, [...args])
);
return Promise.all(defers);
}
...
}
// use
ins.$promiseEmit('init', data).then(...)
複製程式碼
不過emit 的 Promise化可能沒有普適場景。事件的繫結順序,會影響.then 的接收引數的順序;並且按Promise.all 的工作方式,如果有任何地方繫結的事件執行出錯,都會影響resolve的執行。所以,僅在特殊場景下,在明確event例項的使用範圍的時候才考慮使用
附相關程式碼
FEvent最終實現: FEvent