聊聊javascript事件的使用姿勢

舍掉英熊1發表於2019-03-31

前情提要 —— 一種常見的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()的方式作為事件模組使用,這樣所有事件都指向同一個事件例項。一般倒也沒什麼問題,但是中大型應用中事件的管理可能會有點麻煩:

  1. 需要留意 eventName 的定義:為了避免重名可能通常會需要加上一些字首,eg:
event.$on('article-update', doSth)
event.$on('article-tag-update', doSth)
event.$on('article-title-update', doSth)
複製程式碼
  1. 關注事件解綁:事件例項是一個全域性的例項,被繫結的事件及其引用的上下文環境,在不解綁的情況下,將不能被釋放,這可能會引起記憶體洩漏。例子參考:記一次網頁記憶體溢位分析及解決實踐

     不過以上兩點,總是需要人為去注意。多人協作、或者個人狀態不好,難免出現差錯。
    複製程式碼

與例項繫結

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 定義上麻煩一點,但是避免了全域性事件方式的一些困擾:

  1. 一般不用擔心事件繫結帶來的記憶體洩漏問題,只要例項不被引用,不用擔心繫結的事件會駐留記憶體
  2. 不用擔心事件重名問題
  3. 特定場景下減少事件引數的判斷,比如以下場景:
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

相關文章