简介
在组件化开发越来越流行的今天,事件系统演者着非常重要的角色,它经常作为组件间通讯的桥梁。 本文将讨论如何用 JavaScript 实现一个简单的事件系统。
基本结构
我们先回想一下使用事件系统的方式。 通常,我们要先通过 on / listen 方法注册为一个事件注册一个回调函数。 然后调用 emit / fire 来触发一个事件,该事件对应的回调函数就会一一触发。 这样就是我们平常使用的事件系统的工作方式。
事件系统本质上是一个键值对的合集,一个事件名对应多个函数。 在 JavaScript 中,我们很容易用一个 Object 来模拟这种行为。如:
{
ADD_ENTITY: [fn1, fn2, fn3],
REMOVE_ENTITY: [fn4],
UPDATE_ENTITY: [fn5, fn6]
}
复制代码
上面的结构表示有三个事件:ADD_ENTITY, REMOVE_ENTITY, UPDATE_ENTITY,它们对应了一些回调函数。 当 ADD_ENTITY 触发的时候,fn1, fn2, fn3 都会依次调用,如此类推。
on & off & fire
接下来我们将利用上述的结构来实现事件系统。其中最基本的三个操作是:
- on 监听事件
- off 移除事件
- fire 触发事件
按照上述的结构,我们很容易可以写出如下的代码:
const Event = (function () {
const self = this;
self.queue = {};
self.fired = [];
return {
fire: function (event) {
let queue = self.queue[event];
if (typeof queue === "undefined") {
return;
}
queue.forEach((callback) => {
callback();
});
self.fired[event] = true;
},
on: function (event, callback) {
if (self.fired[event] === true) {
return callback();
}
if (typeof self.queue[event] === "undefined") {
self.queue[event] = [];
}
self.queue[event].push(callback);
},
off: function (event) {
delete self.queue[event];
}
};
})();
复制代码
上面的代码非常简单,on 和 off 只是在操作 Object,为其添加、更新或删除一些键值对。 而 fire 方法只是根据给定的事件名来调用它相关的回调函数。
这样,我们就实现了一个简单的事件系统:
Event.on("a", () => {
console.info(1);
});
Event.on("b", () => {
console.info(2);
});
Event.off("a");
Event.fire("a");
Event.fire("b");
// 2
复制代码
once
有些时候,我们需要指定某些回调函数只触发一次。 如:当页面加载完成后,执行初始化操作,这个操作就只会执行一次,之后再执行就不会生效。 上面的事件系统无法实现这种需求,因为每次触发事件,所有绑定的回调事件都会执行,不存在只执行一次的情况。
其实,我们只需要在执行回调函数后把该函数从列表中移除,就可以实现这种需求。 在这之前,我们先想想应该如何存储这些需要移除的函数。 存储方式大概有两种,一种是和不需要移除的函数保存在一起,即:
{
ADD_ENTITY: [
{ callback: fn1, once: false },
{ callback: fn2, once: true }
],
REMOVE_ENTITY: [
{ callback: fn3, once: true }
]
}
复制代码
这种方式在用 emit 触发事件的时候,需要根据 once 属性来判断是否需要移除该回调函数。 另外,这种方式还有一个好处,它能保证回调函数的调用顺序。
另一种方式则是分开存储,通过引入一个新的变量来区分一次生效和多次生效的回调函数,如:
this._events = {
ADD_ENTITY: [fn1, fn2],
REMOVE_ENTITY: [],
};
this._onceHandlers = {
ADD_ENTITY: [fn2],
REMOVE_ENTITY: [fn3]
}
复制代码
这种方式把两种函数隔离开,在移除回调函数的时候变得非常方便,直接移除整个 key 即可。
无论我们选择哪种存储方式,对 once 的实现影响都不大,因为只需要对 on 方法做一点修改就可以了。 同样,对于删除函数的操作,第一种方式只需要找出 once 为 true 的列表索引,然后根据索引来移除即可,而第二种方式直接删除 key 就可以了。 实现起来都比较简单,因此就不给出示例代码了。