事件队列的作用和用途在这里不再赘述,本案例的目标是在于手写一个类似于pubsub-js
这样的事件队列库,实现该库中的以下方法:
subscribe
订阅publish
发布unsubscribe
取消订阅clearAllSubscriptions
清除所有的订阅getSubscriptions
获取订阅countSubscriptions
统计订阅- 错误处理
实现事件队列库后,分别在vue
、react
中引用该库,实现兄弟组件通信和跨级组件通信,组件之间的关系架构图如下:
要求如下:
App
是根组件,在App
组件中,引入子组件1
和子组件2
。- 在
App
组件中,包含了数据count
、mes
以及方法addCount
。其中count
数据类型为Number
,通过addCount
方法可以改变count
的值。 - 在
子组件1
中,包含了自身的数据a
、b
以及方法addA
和addB
,分别用来改变a
和b
的值。并且引用了App
中的数据a
和方法addCount
。 - 在
子组件2
中,要求可以通过点击按钮调用子组件1的addA
和addB
方法。并且在子组件2
中,引入子组件3
。 - 在
子组件3
中,要求可以通过点击按钮调用子组件1的addA
和addB
方法。
- vue
- react
- typescript
# 安装依赖
pnpm i
# 运行
pnpm dev
# 运行成功之后 在浏览器打开 http://localhost:5300
关于webpack
如何搭建typescript
、vue
以及react
环境,由于并不是该节内容的主要目标,因此这里不再详解,不懂或感兴趣的可以参考本节代码地址或往期文章
// 导入pubsub
import pubsub from 'pubsub'
/**
* 订阅主题
* 订阅主题为 add 的主题,并且传入该主题变化之后的处理函数
* @param topic 主题
* @param callback 主题下的事件处理函数
* @returns {string} uid 返回该主题的uid
*/
const uid = pubsub.subscribe('add', (...rest) => {
console.log('add主题发生了变化', rest)
})
/**
* 发布主题
* 出发主题为 add 的主题,并且传入想传入的参数
* @param topic 主题
* @param rest 参数
* @returns {boolean} result 是否发布成功
*/
pubsub.publish('add', { id: 1, name: 'web' })
/**
* 取消订阅 支持通过uid和mes(前置匹配)来清除
* 匹配顺序 mes > uid
* 如果匹配到了前置匹配,那么优先清除前置匹配到,不清除uid
* @param params 需要清除的uid | uid数组 | mes | mes数组
* @param deep 针对mes,是否深度匹配,比如 mes为 'a'时,清除'a'和'a.*'的所有
*/
pubsub.unsubscribe(uid) // 取消uid
pubsub.unsubscribe(['a', 'b']) // 取消所有以 a 和 b 开头的主题
/**
* 统计订阅 仅支持订阅名称(精准匹配)不支持uid
* @param topic 订阅的名称
*/
countSubscriptions('aa')
// 清除所有的订阅
clearAllSubscriptions()
/**
* 查看当前类型下的所有订阅主题 (前置匹配) 不支持uid
* @param topic 订阅主题的名称 比如 topic='x'时,可以查看到['x', 'xx', 'x1']
*/
getSubscriptions('x')
首先,我们需要记录某一个主题(topic)下的所有处理函数(callback),这时我们想到了JSON
的键值对格式,如下所示:
// 主题与主题对应处理函数的关系
const queue = {
addA: [fn, fn, fn],
addB: [fn, fn, fn]
}
但是我们不能直接将处理函数放入对应主题的函数列表中,因为在unsubscribe
方法中,需要根据uid
或topic
来取消订阅主题,也就是删除某一个主题函数列表中的fn
,但是如果按照上面的结构,我们很难知道具体是要删除哪一个fn
。
因此我们需要改变一下数据结构:主题列表中不再存放事件处理函数fn
,而是存放订阅主题后返回的uid。并且使用其他的变量来记录uid
和fn
之间的关系。如下所示:
// 主题与订阅uid的关系
const queue = {
addA: [uid1, uid2, uid3],
addB: [uid4, uid5, uid6]
}
// uid与主题对应处理函数的关系
const uidList = {
uid1: { topic: addA, callback: fn1 },
uid4: { topic: addB, callback: fn4 },
uid3: { topic: addA, callback: fn3 },
...
}
这样我们想要取消订阅uid3
这个主题,我们只需要先在uidList
中找到uid3
对应的值{ topic: addA, callback: fn3 }
,然后从uidList
中移除uid3
,再根据 topic: addA
去queue
中移除addA
中的uid3
即可。逻辑代码如下:
// PS: 以下代码为逻辑演示代码,并非实际运行代码,重点在于思想
// 假设我们要取消订阅 uid3
// 这里我们将 object类型替换为map类型、array类型替换为set类型
const queue = new Map()
const uidList = new Map()
queue.set('addA', uid1)
queue.set('addA', uid2)
queue.set('addB', uid3)
uidList.set(uid1, { topic: 'addA', callback: fn1 })
uidList.set(uid2, { topic: 'addA', callback: fn2 })
uidList.set(uid3, { topic: 'addB', callback: fn3 })
// 第一步 先从 uidList 中找到uid3的topic信息
const topicInfo = uidList.get(uid3) // { topic: 'addB', callbcak: fn3 }
// 第二步 从 uidList 中移除uid3
uidList.delete(uid3)
// 第三步 从 queue 中删除 addB 中的uid3,并重新赋值 addB
const queueInfo = queue.get(topicInfo.topic)
queueInfo.delete(uid3)
queue.set(topicInfo.topic, queueInfo)
我们期望提供一个唯一实例,这样在整个应用程序中共享相同状态或资源。因此我们采用单例模式。
// pubsub.ts 导出
class Pubsub {
...
}
// 在内部实例化,这样全局都调用的是同一个实例
const pubsub = new Pubsub()
export default pubsub
// a.ts文件 导入
import pubsub from './pubsub'
pubsub.subscribe('addA', () => {})
// b.ts文件 导入
import pubsub from './pubsub'
pubsub.publish('addA', 11)
本案例采用TypeScript
编写,除了函数重载
部分,别的与JavaScript
一致
class Pubsub {
private queue: Map<string, Set<string>>
private uidList: Map<string, any>
private uid: number
constructor() {
this.queue = new Map()
this.uidList = new Map()
this.uid = 0
}
}
const pubsub = new Pubsub()
export default pubsub
/**
* 订阅主题
* @param topic 主题名称
* @param callback 该主题对应的处理函数
* @returns {string} uid
*/
subscribe(topic: string, callback: () => any): string {
const topicInfo = this.queue.get(topic) || new Set()
const uid = `uid_${this.uid}`
topicInfo.add(uid)
this.uidList.set(uid, {
topic,
callback
})
this.queue.set(topic, topicInfo)
this.uid++
return uid
}
/**
* 发布主题
* @param topic 主题
* @param rest 参数
* @returns {boolean} result 是否发布成功
*/
publish(topic: string, ...rest: any[]): boolean {
const topicInfo = this.queue.get(topic) //map
if (!topicInfo) return false
if (!topicInfo.size) return false
topicInfo.forEach((uid) => {
const callback = this.uidList.get(uid)?.callback
callback?.(topic, ...rest)
})
return true
}
/**
* 取消订阅 支持通过uid和topic(主题名称前置匹配)来清除
* 匹配顺序 topic > uid
* 如果匹配到了前置匹配,那么优先清除前置匹配到,不清除uid
* @param params 需要清除的uid | uid数组 | topic | topic数组
* @param deep 针对topic,是否深度匹配,比如 topic为 'a'时,清除'a'和'a.*'的所有
* @returns {string | boolean} 如果传入的是uid,则返回boolean,如果传入的是topic,则返回topic
*/
unsubscribe(uid: string | string[]): string | boolean
unsubscribe(topic: string | string[], deep: boolean): string | boolean
unsubscribe(params: string | string[], deep = true) {
// 是否匹配到前置匹配 如果匹配到之后 不再继续删除uid
let isDelete = false
if (Array.isArray(params)) {
params.forEach((u) => {
// topic
this.queue.forEach((list, key) => {
// deep ? 深度匹配 :全等
if (deep ? key.startsWith(u) : key === u) {
list.forEach((uid) => {
this.uidList.delete(uid)
})
this.queue.delete(key)
isDelete = true
}
})
if (isDelete) return true
// uid
const isHas = this.uidList.has(u)
if (!isHas) return false
const info = this.uidList.get(u)
const queueInfo = this.queue.get(info.topic)
if (!queueInfo || !queueInfo.size) return true
queueInfo.delete(u)
this.uidList.delete(u)
this.queue.set(info.topic, queueInfo)
!queueInfo.size && this.queue.delete(info.topic)
return params
})
} else {
// topic
this.queue.forEach((uidList, key) => {
// deep ? 深度匹配 :全等
if (deep ? key.startsWith(params) : key === params) {
uidList.forEach((uid) => {
this.uidList.delete(uid)
})
this.queue.delete(key)
isDelete = true
}
})
if (isDelete) return true
// uid
const isHas = this.uidList.has(params)
if (!isHas) return false
const info = this.uidList.get(params)
const queueInfo = this.queue.get(info.topic)
if (!queueInfo) return true
queueInfo.delete(params)
this.uidList.delete(params)
this.queue.set(info.topic, queueInfo)
!queueInfo.size && this.queue.delete(info.topic)
return params
}
}
clearAllSubscriptions(): void {
this.queue.clear()
this.uidList.clear()
this.uid = 0
}
/**
* 统计订阅 仅支持订阅名称(精准匹配)不支持uid
* @param topic 订阅的名称
*/
countSubscriptions(topic: string | string[]): number | object {
if (Array.isArray(topic)) {
const topicInfo = {} as any
topic.forEach((m) => {
topicInfo[m] = this.queue.get(m)?.size || 0
})
return topicInfo
} else {
return this.queue.get(topic)?.size || 0
}
}
/**
* 查看当前类型下的所有订阅主题 (前置匹配) 不支持uid
* @param topic 订阅主题的名称 比如 topic='x'时,可以查看到['x', 'xx', 'x1']
*/
getSubscriptions(topic: string | string[]): string[] {
const topicList: Array<string> = []
if (Array.isArray(topic)) {
topic.forEach((item) => {
this.queue.forEach((_, key) => {
if (key.startsWith(item)) {
topicList.push(key)
}
})
})
} else {
this.queue.forEach((_, key) => {
if (key.startsWith(topic)) {
topicList.push(key)
}
})
}
return topicList
}