前端需要掌握的20个手写功能—Event Emitter/发布订阅模式
前言
该系列是笔者整理的前端需要掌握的手写功能集合,这些功能的手撕需要前端开发者具有一定基础功底,在面试中也会高频的出现。笔者会将每个手写功能单独呈现为一篇,尽可能整理的细致,同时也不会让文章篇幅太长,内容太过杂乱,该篇为第一个手写功能,手写实现Event Bus/ Event Emitter。
框架中的EventBus
笔者在初次接触EventBus概念是在使用Vue的时候,用EventBus实现跨组件通讯,一般使用场景为A、B组件相隔很远,但是又想要有所通讯,一般的父传子props、子传父$emit在这种场景下无法使用。因此就引入了EventBus事件总线的方法,下面简单的回顾下Vue中的EventBus的使用
一:创建一个EventBus (本质上是Vue的一个实例对象)
const EventBus = new Vue()
export default EventBus
二:将EventBus挂载到全局
import bus from "EventBus的文件路径"
Vue.prototype.bus = bus
三: 订阅事件
this.bus.$on('someEvent',func)
四: 发布事件
this.bus.$emit('someEvent',params)
通过这四步我们就能在Vue中实现远距离组件间的通讯,无法直接通讯的组件会借助一个中间桥梁(EventBus)进行相互通讯;
或者如果你曾经是使用React做前端开发,那么你可能使用过EventEmitter这个库,他的大概使用方式我们也来简单的回顾一下
import React, { Component } from 'react'
//一: 导入EventEmitter
import { EventEmitter } from 'events';
//二: 构建事件实例
const EventBus = EventEmitter()
//tips:我们可以在多个组件中去增加同一个事件的订阅,这里仅仅是示例
class Observer extends Component {
componentDidMount () {
// 三: 增加事件订阅
this.event1 = EventBus.addListener("someEvent", (params) => {
console.log(params)
})
}
componentWillUnMount () {
//四: 移除事件订阅
EventBus.removeListener(this.event1)
}
render () {
return (
<div>
事件监听组件
</div>
)
}
}
class Publisher extends Component {
handleClick () {
const params = {}
//五: 发布事件(当someEvent发布时,订阅该事件的函数就会执行)
EventBus.emit('someEvent', params)
}
render () {
return (
<div>
<button onClick={this.handleClick.bind(this)}>发布事件</button>
</div>
)
}
}
以上就是我们在框架中使用的EventBus或者Event Emitter,他们的作用是让远距离关系的组件,借由自身完成信息专递,一般我们称之为全局事件总线
全局事件总线的设计就使用到了前端最重要的设计模式之一发布订阅模式,一般使用场景有以下特点:
- 组件或者事物之间需要通讯,如进行变化通知,但是两者并没有或者无法直接进行通讯(彼此无感知),而是借助于第三方进行代理
- 在发布方和订阅方上,存在有一对多的关系,一个发布者可以对应多个订阅者,当发布者发布信息时,订阅这条信息的订阅方都会进行响应
补充:如果你有了解过观察者模式,你会发现这个类似观察者模式,但是其实两者存在区别,关键点在于是否存在第三方桥梁,观察者模式是由观察者同被观察者直接通讯,两者能够相互感知,逻辑上存在一定的耦合(这种耦合并不是说不好,而是由于两者之间本就存在功能上的关系依赖),而发布订阅模式是订阅方和发布方无需直接通讯,两者无需感知对方,借由第三方进行协助通讯。
经过上述的列子,即使你没有使用过Event Emitter,也大概了解其工作方式,现在让我们实现一个简易版的Event Emitter
EventBus/Event Emitter的实现
Event Emitter的基础实现如下
class EventEmitter {
constructor() {
// 单例模式
if (!EventEmitter.instance) {
EventEmitter.instance = this
this.handleMap = {}
}
//map结构,用于储存事件与其对应的回调
return EventEmitter.instance
}
//事件订阅,需要接收订阅事件名和对应的回调函数
on (eventName, callback) {
this.handleMap[eventName] = this.handleMap[eventName] ?? []
this.handleMap[eventName].push(callback)
}
//事件发布,需要接收发布事件名和对应的参数
emit (eventName, ...args) {
if (this.handleMap[eventName]) {
//这里需要浅拷贝一下handleMap[eventName],因为在once添加订阅时会修改this.handleMap,若once绑定在前就会导致后一个监听被移除
const handlers = [...this.handleMap[eventName]]
handlers.forEach(callback => callback(...args))
}
}
//移除订阅,需要移除的订阅事件名及指定的回调函数
remove (eventName, callback) {
const callBacks = this.handleMap[eventName]
const index = callBacks.indexOf(callback)
if (index !== -1) {
callBacks.splice(index, 1)
}
}
//添加单次订阅,触发一次订阅事件后取消订阅,需要添加的订阅事件名及指定的回调函数
once (eventName, callback) {
const warpper = (...args) => {
callback(...args)
this.remove(eventName, warpper)
}
this.on(eventName, warpper)
}
}
//基础测试
const eventBus = new EventEmitter()
eventBus.once("demo", (params) => { console.log(1, params) })
eventBus.on("demo", (params) => { console.log(2, params) })
eventBus.on("demo", (params) => { console.log(3, params) })
eventBus.emit("demo", "someData")
以上就是利用发布订阅模式进行的Event Emitter的简单实现,分别实现了on、emit、once、remove四个方法,对于掌握以上的同学可以去查看Event Emitter库的源码,这是一个微量库内容并不多,推荐大家去读一读,看看同样的功能,在这种被广泛使用的类库中是如何实现的,相信会有所收获。
结语
在前端开发中我们会使用诸多别人已经封装好的比较健壮的轮子,在感受便捷的同时,也要尝试去理解去研究其中的设计思想,为何如此设计,这里只是简单的整理了Event Emitter的基础封装。
由发布订阅模式我们可以可以联想到与其相近的观察者模式,Vue的响应式设计就使用到了该模式,感兴趣的同学可以去了解一下,笔者也会对该知识点进行整理后链接到此处。
转载自:https://juejin.cn/post/7034182284081102884