前情提要 —— 一种常见的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