背景
react 的工作模式是单向数据流:指的就是当前组件的 state 以 props 的形式流动时,只能流向组件树中比自己层级更低的组件。
- 父子通信:可以通过父组件传递 props 给子组件来实现
- 子父通信:考虑到 props 是单向的,子组件并不能直接将自己的数据塞给父组件,但 props 的形式也可以是多样的。假如父组件传递给子组件的是一个绑定了自身上下文的函数,那么子组件在调用该函数时,就可以将想要交给父组件的数据以函数入参的形式给出去,以此来间接地实现数据从子组件到父组件的流动。
- 兄弟间通信:需要有一个先决条件,兄弟组件都拥有一个共同的父组件,然后使用父子通信和子父通信的方法实现兄弟间通信
三种通信可以解决大部分的场景,但是假如组件间嵌套的十分深,想通过层层传递 props 的方式解决通信就不合适了,往往会消耗非常多的代码,而且很难维护。
浏览器原生发布-订阅模式
发布-订阅机制早期最广泛的应用,应该是在浏览器的 DOM 事件中。
target.addEventListener(type, listener, useCapture);
通过调用 addEventListener 方法,我们可以创建一个事件监听器,这个动作就是“订阅”。比如我可以监听 click(点击)事件:
el.addEventListener("click", func, false);
这样一来,当 click 事件被触发时,事件会被“发布”出去,进而触发监听这个事件的 func 函数。这就是一个最简单的发布-订阅案例。
使用发布-订阅模式的优点在于,监听事件的位置和触发事件的位置是不受限的,就算相隔十万八千里,只要它们在同一个上下文里,就能够彼此感知。这个特性,太适合用来应对“任意组件通信”这种场景了。
实现发布-订阅模式
发布-订阅模式中有两个关键的动作:事件的监听(订阅)和事件的触发(发布) ,这两个动作自然而然地对应着两个个基本的 API 方法;还有一个事件删除的 API 方法。
- on():负责注册事件的监听器,指定事件触发时的回调函数。
- emit():负责触发事件,可以通过传参使其在触发的时候携带数据 。
- off():负责监听器的删除。
整个发布-订阅模式都被封装在一个类,叫做 EventEmitter,事件和该事件的对应关系需要通过一个容器存储,设为对象 eventMap。
constructor() {
// eventMap 用来存储事件和监听函数之间的关系
this.eventMap= {}
}
订阅 on()
所谓“订阅”,也就是注册事件监听函数的过程。这是一个“写”操作,具体来说就是把事件和对应的监听函数写入到 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)
}
发布 emit()
订阅操作是一个“写”操作,相应的,发布操作就是一个“读”操作。发布的本质是触发安装在某个事件上的监听函数,我们需要做的就是找到这个事件对应的监听函数队列,将队列中的 handler 依次执行出队:
// 触发时是可以携带数据的,params 就是数据的载体
emit(type, param) {
// 假如该事件没有订阅,抛出异常
if(!this.eventMap[type]) {
throw new Error("大哥,不存在这个事件")
}
// 假设该事件是有订阅的(对应的事件队列存在)
if(this.eventMap[type]) {
// 将事件队列里的 handler 依次执行出队
this.eventMap[type].forEach((handler, index)=> {
// 读取 params
handler(params)
})
}
解除 off()
off(type, handler) {
if(this.eventMap[type]) {
this.eventMap[type].splice(this.eventMap[type].indexOf(handler)>>>0,1)
}
}
splice 的第一个参数是负数时,会从数组的最后往前找。如果传入一个不存在的函数给 off 方法,indexOf 找不到会返回 -1 ,再调用 splice 就会将队列中最后一个函数删除掉了。而使用无符号右移,-1 无符号右移的结果为 4294967295,这个数足够大,不会对原队列造成影响。
类 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]) {
throw new Error("大哥,不存在这个事件")
}
// 假设该事件是有订阅的(对应的事件队列存在)
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);
}
}
}