前言
常规的发布订阅无论是在类型校验还是在语法提示上都是一大痛点,对于不同的事件名以及传递的参数数据类型,都犹如一个盲盒,对于不熟悉它的人来说,维护它同于维护si山。
初级运用(基于内置规则)
高级运用(在基础规则之上进行自定义扩展)
写个普通的发布订阅类
interface EventBusItem {
fn: Function
once: boolean
}
export default class EventBus {
public pool: Map<string, EventBusItem[]> = new Map()
/**
* 事件绑定(once 模式)
* @param type 事件名
* @param fn 回调函数
* @param once 回调函数只执行一次
* @returns
*/
public on(type: string, fn: Function, once: boolean = false) {
let row = this.pool.get(type)
if (!row) {
this.pool.set(type, (row = []))
}
row.push({ fn, once })
return this
}
/**
* 事件解绑
* @param type 事件名
* @param fn 回调函数
* @returns
*/
public off(type: string, fn: Function) {
const row = this.pool.get(type)
if (row) {
for (let i = 0; i < row.length; i++) {
if (row[i].fn === fn) {
row.splice(i--, 1)
}
}
}
return this
}
/**
* 事件发布
* @param type 事件名
* @param e 回调函数的参数
* @returns
*/
public emit(type: string, arg: any) {
const row = this.pool.get(type)
if (row) {
for (let i = 0; i < row.length; i++) {
const { fn, once } = row[i]
fn(arg)
if (once) {
row.splice(i--, 1)
}
}
}
return this
}
}
复制代码
然后我们来看一下它的实际使用体验,我们会发现不仅没有语法提示,在传递数据上也毫无约束力
写一个有语法提示的发布订阅
经常写原生的同学应该有注意到,DOM 原素的 addEventListener
方法不仅有语法提示的,回调方法的参数还有类型限制,比如:
然后我查看了下它的声明文件
那么接下来我们就模仿 addEventListener
对我们的发布订阅类进行优化
interface EventBusItem {
fn: Function
once: boolean
}
interface EventBusMap {
render: number[]
connected: undefined
attributeChaged: { attr: string; nval: string | null; oval: string | null }
}
export default class EventBus {
// 此处的 keyof 原示例请查看
// https://www.typescriptlang.org/docs/handbook/2/keyof-types.html
// https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html
public pool: Map<keyof EventBusMap, EventBusItem[]> = new Map()
// 此处的 extends 原示例请查看
// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
public on<K extends keyof EventBusMap>(type: K, fn: (arg: EventBusMap[K]) => void, once: boolean = false) {}
// 此处的 extends keyof 原示例请查看
// https://www.typescriptlang.org/docs/handbook/2/generics.html#using-type-parameters-in-generic-constraints
public off<K extends keyof EventBusMap>(type: K, fn: (arg: EventBusMap[K]) => void) {}
// 此处的 EventBusMap[K] 原示例请查看
// https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html
public emit<K extends keyof EventBusMap>(type: K, arg: EventBusMap[K]) {}
}
复制代码
我们来体验一下优化后的发布订阅
让发布订阅支持自定义扩展
为了此发布订阅的通用性,我们还得继续对其进行优化
interface EventBusItem {
fn: Function
once: boolean
}
// 禁止在此接口(或继承此接口的接口)中使用 [key: string]: any 或 [propName: string]: any
// 如果使用必将导致提示失效,这将使定义的泛型失去意义
// 同时此接口必须要包含内容
export interface EventBusMap {
render: number[]
connected: undefined
attributeChaged: { attr: string; nval: string | null; oval: string | null }
}
// 此处的 extends 原示例请查看
// https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-classes
export default class EventBus<M extends EventBusMap> {
public pool: Map<keyof M, EventBusItem[]> = new Map()
public on<K extends keyof M>(type: K, fn: (arg: M[K]) => void, once: boolean = false) {}
public off<K extends keyof M>(type: K, fn: (arg: M[K]) => void) {}
public emit<K extends keyof M>(type: K, arg: M[K]) {}
复制代码
效果预览
完整代码
interface EventItem {
fn: Function
once: boolean
}
// 使用 * 解决此接口不能为空的问题
export interface EventsMap {
'*': any
}
// 将回调函数的类型进行抽离
type EventFN<E extends EventsMap, K extends keyof E> = (e: E[K]) => void
/**
* 通用发布订阅管理器
* @example
* // --------- 常规使用 ---------
* const events = new Events<EventsMap>()
* events.on('render', function(e) {
* // e 的类型将自动判定为 HTMLAst
* })
*
* // ----- 添加自定义事件类型 -----
* interface CumtomEventsMap extends EventsMap {
* click: MouseEvent
* }
* const events = new Events<CumtomEventsMap>()
* events.on('click', function(e) {
* // e 的类型将自动判定为 MouseEvent
* })
*/
export default class Events<E extends EventsMap> {
public pool: Map<keyof E, EventItem[]> = new Map()
/**
* 事件绑定
* @param type 事件名
* @param fn 回调函数
* @param once 回调函数只执行一次
* @returns
*/
public on<K extends keyof E>(type: K, fn: EventFN<E, K>, once: boolean = false) {
let row = this.pool.get(type)
if (!row) {
this.pool.set(type, (row = []))
}
row.push({ fn, once })
return this
}
/**
* 事件绑定(once 模式)
* @param type 事件名
* @param fn 回调函数
* @param once 回调函数只执行一次
* @returns
*/
public once<K extends keyof E>(type: K, fn: EventFN<E, K>) {
return this.on(type, fn, true)
}
/**
* 事件解绑
* @param type 事件名
* @param fn 回调函数
* @returns
*/
public off<K extends keyof E>(type: K, fn: EventFN<E, K>) {
const row = this.pool.get(type)
if (row) {
for (let i = 0; i < row.length; i++) {
if (row[i].fn === fn) {
row.splice(i--, 1)
}
}
}
return this
}
/**
* 事件发布
* @param type 事件名
* @param e 回调函数的参数
* @param thisArg 回调函数的 this 指向
* @returns
*/
public emit<K extends keyof E>(type: K, e: E[K], thisArg?: any) {
const row = this.pool.get(type)
if (row) {
for (let i = 0; i < row.length; i++) {
if (row[i].fn === fn) {
row.splice(i--, 1)
}
}
}
return this
}
/**
* 是否对某个事件进行了回调函数的绑定
* @param type 事件名
* @returns
*/
public has<K extends keyof E>(type: K) {
return this.pool.get(type)?.length ? true : false
}
/**
* 在指定 Events 实例的基础上进行类型扩展(内部的事件缓存池将进行深度克隆)
* @param target 被扩展的 Events 实例
* @returns
*/
public static extends<M extends EventsMap, N extends M>(target: Events<M>) {
const events = new Events<N>()
target.pool.forEach(function (row, type) {
events.pool.set(
type,
row.map(({ fn, once }) => ({ fn, once }))
)
})
return events
}
}
复制代码
总结
虽然实现了发布订阅的语法提示和类型检查,但是同样的,在被允许的类型之外的都将会报错,对于这一点暂时未想到好的办法…
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END