大家好,我是前端西瓜哥。
事件订阅是模块间解耦的常见方式。
比如相隔遥远的两个组件,可以通过一个订阅,一个发布的方式,实现数据通信。
下面我们来看看事件订阅的几种设计风格。
监听器函数
第一种是 使用监听器函数本身作为标识符。
常见的场景有 DOM 事件的绑定:
const handler = () {
// do something
}
window.addEventListener('resize', handler); // 绑定事件
window.removeEventListener('resize', handler); // 取消事件
此外还有 Node.js 的 EventEmitter 类,很多支持监听事件的类都继承了它。用法为:
import { EventEmitter } from "node:events";
const myEmitter = new EventEmitter();
const handler = () {
console.log("前端西瓜哥");
};
myEmitter.on("event", handler); // 绑定事件
myEmitter.emit("event"); // 打印了内容
myEmitter.off("event", handler); // 取消事件
myEmitter.emit("event"); // 无事发生
原理很简单,就是维护一个映射表上,key 为事件名,value 为要顺序执行的监听器,大概这样:
{
'resize': [handler1, handler2],
'click': [handler3, handler4]
}
一个监听器函数就是一个唯一的对象,通过它可以找出在对应事件下的位置,将其从列表中移除,就算是取消了事件绑定。
取消绑定逻辑大概为:
const index = map[eventName].indexOf(handler);
if (index !== -1) {
map[eventName].splice(index, 1);
}
订阅 id
通过一个 id 来代表绑定的监听器。
经典场景为 setTimeout:
const timeoutId = setTimeout(() {
// ...
}, 1000); // 订阅
clearTimeout(timeoutId); // 取消
实现原来基本类似前一种方式,只是改为用 id 来作为标识。
{
[eventName]: [
{id: 1, hander: handler1},
{id: 2, hander: handler2},
]
}
返回封装好的取消绑定方法
上面两种写法,都需要一个变量额外保存标识,然后再使用事件订阅对象专门的取消订阅函数,难免有点繁琐。
我们对第一种风格,可以做一个封装:
const bindEvent = (target, eventName, handler) => {
target.addEventListener('resize', handler); // 绑定
return () {
target.removeEventListener('resize', handler); // 取消
}
}
const unBindEvent = bindEvent(window, 'resize', handler); // 封装的绑定
unBindEvent(); // 封装的取消
这种写法直接调用返回的函数即可解绑,不需要保存 id 和一个专门的取消订阅函数,代码更简洁。
常见场景是 React 的 useEffect:
const App = () {
useEffect(() {
const handler = () {
// ...
}
window.addEventListener('resize', handler); // 绑定事件
return () {
window.removeEventListener('resize', handler); // 取消事件
}
}, [])
}
结尾
三种风格,第一种是监听器本身作为标识,第二种是使用额外的一个 id(数字或字符串)来代表监听器,第三种是在调用监听方法时,直接返回一个可以取消监听的函数。