发布-订阅模型 API 设计思路
发布-订阅模式中有两个关键的动作:事件的监听(订阅)和事件的触发(发布),这两个动作自然而然地对应着两个基本的 API 方法。
-
on():负责注册事件的监听器,指定事件触发时的回调函数。
-
emit():负责触发事件,可以通过传参使其在触发的时候携带数据 。
最后,只进不出总是不太合理的,我们还要考虑一个 off() 方法,必要的时候用它来删除用不到的监听器:
-
off():负责监听器的删除。
发布-订阅模型编码实现
在写代码之前,先要捋清楚思路。这里我把“实现 EventEmitter”这个大问题,拆解为 3 个具体的小问题,下面我们逐个来解决。
问题一:事件和监听函数的对应关系如何处理?
提到“对应关系”,应该联想到的是“映射”。在 JavaScript 中,处理“映射”我们大部分情况下都是用对象来做的。所以说在全局我们需要设置一个对象,来存储事件和监听函数之间的关系:
constructor() {
// eventMap 用来存储事件和监听函数之间的关系
this.eventMap= {}
}
问题二:如何实现订阅?
所谓“订阅”,也就是注册事件监听函数的过程。这是一个“写”操作,具体来说就是把事件和对应的监听函数写入到 eventMap 里面去:
// type 这里就代表事件的名称
on(type, handler) {
// hanlder 必须是一个函数,如果不是直接报错
if(!(handler instanceof Function)) {
throw new Error("哥 你错了 请传一个函数")
}
// 判断 type 事件对应的队列是否存在
if(!this.eventMap[type]) {
// 若不存在,新建该队列
this.eventMap[type] = []
}
// 若存在,直接往队列里推入 handler
this.eventMap[type].push(handler)
}
问题三:如何实现发布?
订阅操作是一个“写”操作,相应的,发布操作就是一个“读”操作。发布的本质是触发安装在某个事件上的监听函数,我们需要做的就是找到这个事件对应的监听函数队列,将队列中的 handler 依次执行出队:
// 别忘了我们前面说过触发时是可以携带数据的,params 就是数据的载体
emit(type, params) {
// 假设该事件是有订阅的(对应的事件队列存在)
if(this.eventMap[type]) {
// 将事件队列里的 handler 依次执行出队
this.eventMap[type].forEach((handler, index)=> {
// 注意别忘了读取 params
handler(params)
})
}
}
到这里,最最关键的 on 方法和 emit 方法就实现完毕了。最后我们补充一个 off 方法:
off(type, handler) {
if(this.eventMap[type]) {
this.eventMap[type].splice(this.eventMap[type].indexOf(handler)>>>0,1)
}
}
一个核心功能完备的 EventEmitter 如下:
class myEventEmitter {
constructor() {
// eventMap 用来存储事件和监听函数之间的关系
this.eventMap = {};
}
// type 这里就代表事件的名称
on(type, handler) {
// hanlder 必须是一个函数,如果不是直接报错
if (!(handler instanceof Function)) {
throw new Error("哥 你错了 请传一个函数");
}
// 判断 type 事件对应的队列是否存在
if (!this.eventMap[type]) {
// 若不存在,新建该队列
this.eventMap[type] = [];
}
// 若存在,直接往队列里推入 handler
this.eventMap[type].push(handler);
}
// 别忘了我们前面说过触发时是可以携带数据的,params 就是数据的载体
emit(type, params) {
// 假设该事件是有订阅的(对应的事件队列存在)
if (this.eventMap[type]) {
// 将事件队列里的 handler 依次执行出队
this.eventMap[type].forEach((handler, index) => {
// 注意别忘了读取 params
handler(params);
});
}
}
off(type, handler) {
if (this.eventMap[type]) {
this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1);
}
}
}
测试
简单的测试
下面我们对 myEventEmitter 进行一个简单的测试,创建一个 myEvent 对象作为 myEventEmitter 的实例,然后针对名为 “test” 的事件进行监听和触发:
// 实例化 myEventEmitter
const myEvent = new myEventEmitter();
// 编写一个简单的 handler
const testHandler = function (params) {
console.log(`test事件被触发了,testHandler 接收到的入参是${params}`);
};
// 监听 test 事件
myEvent.on("test", testHandler);
// 在触发 test 事件的同时,传入希望 testHandler 感知的参数
myEvent.emit("test", "newState");
React任意的两个组件 A 和 B,进行通信
,以数据从 A 流向 B 为例:
在 B 中编写一个handler(记得将这个 handler 的 this 绑到 B 身上),在这个 handler 中进行以 B 为上下文的 this.setState 操作,然后将这个 handler 作为监听器与某个事件关联起来。比如这样:
// 注意这个 myEvent 是提前实例化并挂载到全局的,此处不再重复示范实例化过程
const globalEvent = window.myEvent
class B extends React.Component {
// 这里省略掉其他业务逻辑
state = {
newParams: ""
};
handler = (params) => {
this.setState({
newParams: params
});
};
bindHandler = () => {
globalEvent.on("someEvent", this.handler);
};
render() {
return (
<div>
<button onClick={this.bindHandler}>点我监听A的动作</button>
<div>A传入的内容是[{this.state.newParams}]</div>
</div>
);
}
}
接下来在 A 组件中,只需要直接触发对应的事件,然后将希望携带给 B 的数据作为入参传递给 emit 方法即可。代码如下:
class A extends React.Component {
// 这里省略掉其他业务逻辑
state = {
infoToB: "哈哈哈哈我来自A"
};
reportToB = () => {
// 这里的 infoToB 表示 A 自身状态中需要让 B 感知的那部分数据
globalEvent.emit("someEvent", this.state.infoToB);
};
render() {
return <button onClick={this.reportToB}>点我把state传递给B</button>;
}
}