Skip to content

Commit

Permalink
feat(EventBus): 支持 beforeEmit, beforeOn, tag
Browse files Browse the repository at this point in the history
  • Loading branch information
fjc0k committed Aug 26, 2020
1 parent a82afdc commit ed28ce6
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 28 deletions.
83 changes: 82 additions & 1 deletion src/utils/EventBus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type Events = {
error: (payload: { message: string }) => any
}

describe(EventBus.name, () => {
describe('EventBus', () => {
test('可订阅发布', () => {
const bus = new EventBus<Events>()
const enterCallback = jest.fn()
Expand Down Expand Up @@ -87,4 +87,85 @@ describe(EventBus.name, () => {
const results = bus.emit('enter')
expect(results).toEqual([1, 2])
})

test('支持 beforeOn', () => {
const bus = new EventBus<Events>({
beforeOn: {
error(cb) {
cb.__EVENT_BUS_TAG__ = 200
return cb
},
},
})
const errorCallback = jest.fn().mockImplementation(() => 1)
const errorCallback2 = jest.fn().mockImplementation(() => 2)
bus.on('error', errorCallback)
bus.on('error', errorCallback2)
const results = bus.emit('error', { message: 'hello' })
expect(results).toEqual([1, 2])
expect(errorCallback).toBeCalled().toBeCalledTimes(1)
expect(errorCallback2).toBeCalled().toBeCalledTimes(1)
bus.off('error', 200)
const results2 = bus.emit('error', { message: 'hello2' })
expect(results2).toEqual([])
expect(errorCallback).toBeCalled().toBeCalledTimes(1)
expect(errorCallback2).toBeCalled().toBeCalledTimes(1)
})

test('支持 beforeEmit', () => {
const bus = new EventBus<Events>({
beforeEmit: {
error() {
this.emit({
name: 'success',
})
},
},
})
const errorCallback = jest.fn().mockImplementation(() => 1)
const errorCallback2 = jest.fn().mockImplementation(() => 2)
const successCallback = jest.fn().mockImplementation(() => 'x')
const successCallback2 = jest.fn().mockImplementation(() => 'y')
bus.on('error', errorCallback)
bus.on('error', errorCallback2)
bus.on('success', successCallback)
bus.on('success', successCallback2)
const results = bus.emit('error', { message: 'hello' })
expect(results).toEqual([1, 2])
expect(errorCallback).toBeCalled().toBeCalledTimes(1)
expect(errorCallback2).toBeCalled().toBeCalledTimes(1)
expect(successCallback).toBeCalled().toBeCalledTimes(1)
expect(successCallback2).toBeCalled().toBeCalledTimes(1)
})

test('支持按 tag emit', () => {
let i = 0
const bus = new EventBus<Events>({
beforeOn: {
error(cb) {
cb.__EVENT_BUS_TAG__ = i++
return cb
},
},
})
const errorCallback = jest.fn().mockImplementation(() => 1)
const errorCallback2 = jest.fn().mockImplementation(() => 2)
bus.on('error', errorCallback)
bus.on('error', errorCallback2)

const results = bus.emit('error', { message: 'hello' })
expect(results).toEqual([1, 2])
expect(errorCallback).toBeCalled().toBeCalledTimes(1)
expect(errorCallback2).toBeCalled().toBeCalledTimes(1)

const results2 = bus.emit({ name: 'error', tag: 0 }, { message: 'hello2' })
expect(results2).toEqual([1])
expect(errorCallback).toBeCalled().toBeCalledTimes(2)
expect(errorCallback2).toBeCalled().toBeCalledTimes(1)

const results3 = bus.emit({ name: 'error', tag: 1 }, { message: 'hello3' })
expect(results3).toEqual([2])
expect(errorCallback).toBeCalled().toBeCalledTimes(2)
expect(errorCallback2).toBeCalled().toBeCalledTimes(2)
})
})
135 changes: 108 additions & 27 deletions src/utils/EventBus.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,46 @@
export type EventBusListenerTag = string | number

export type EventBusListener<
TCallback extends (...args: any[]) => any = (...args: any[]) => any
> = TCallback & {
__EVENT_BUS_TAG__?: EventBusListenerTag
}

export type EventBusOffListener = () => any

export type EventBusListeners = Record<string, EventBusListener>

export interface EventBusListenerDescriptor<
TListenerName extends keyof EventBusListeners
> {
name: TListenerName
context?: any
tag?: EventBusListenerTag
}

export type EventBusBeforeOn<TListeners extends EventBusListeners> = {
[TListenerName in keyof TListeners]?: (
this: EventBus<TListeners>,
callback: EventBusListener<TListeners[TListenerName]>,
) => TListeners[TListenerName]
}

export type EventBusBeforeEmit<TListeners extends EventBusListeners> = {
[TListenerName in keyof TListeners]?: (
this: EventBus<TListeners>,
context: any,
) => any
}

export interface EventBusOptions<TListeners extends EventBusListeners> {
beforeOn?: EventBusBeforeOn<TListeners>
beforeEmit?: EventBusBeforeEmit<TListeners>
}

/**
* 事件巴士,管理事件的发布与订阅。
*
* @public
* @template TEvents 事件名称及其对应的回调描述
* @template TListeners 事件名称及其对应的回调描述
* @example
* ```typescript
* const bus = new EventBus<{
Expand All @@ -13,12 +51,17 @@
* // => 控制台输出: 提交成功
* ```
*/
export class EventBus<TEvents extends Record<string, (...args: any[]) => any>> {
export class EventBus<TListeners extends EventBusListeners> {
/**
* 构造函数。
*/
constructor(private options?: EventBusOptions<TListeners>) {}

/**
* 回调列表。
*/
private callbacks: {
[Key in keyof TEvents]: Array<TEvents[Key]>
[TListenerName in keyof TListeners]: Array<TListeners[TListenerName]>
} = Object.create(null)

/**
Expand All @@ -28,13 +71,15 @@ export class EventBus<TEvents extends Record<string, (...args: any[]) => any>> {
* @param callback 事件触发回调
* @returns 返回取消订阅的函数
*/
on<TName extends keyof TEvents>(
eventName: TName,
callback: TEvents[TName],
): () => any {
on<TListenerName extends keyof TListeners>(
eventName: TListenerName,
callback: TListeners[TListenerName],
): EventBusOffListener {
if (!this.callbacks[eventName]) {
this.callbacks[eventName] = []
}
callback =
this.options?.beforeOn?.[eventName]?.call(this, callback) ?? callback
const index = this.callbacks[eventName].indexOf(callback)
if (index === -1) {
this.callbacks[eventName].push(callback)
Expand All @@ -49,14 +94,14 @@ export class EventBus<TEvents extends Record<string, (...args: any[]) => any>> {
* @param callback 事件触发回调
* @returns 返回取消订阅的函数
*/
once<TName extends keyof TEvents>(
eventName: TName,
callback: TEvents[TName],
): () => any {
once<TListenerName extends keyof TListeners>(
eventName: TListenerName,
callback: TListeners[TListenerName],
): EventBusOffListener {
const off = this.on(eventName, ((...args) => {
off()
callback(...args)
}) as TEvents[TName])
}) as TListeners[TListenerName])
return off
}

Expand All @@ -66,14 +111,26 @@ export class EventBus<TEvents extends Record<string, (...args: any[]) => any>> {
* @param eventName 事件名称
* @param callback 事件触发回调
*/
off<TName extends keyof TEvents>(
eventName: TName,
callback?: TEvents[TName],
off<TListenerName extends keyof TListeners>(
eventName: TListenerName,
callbackOrTag?: TListeners[TListenerName] | string | number,
): void {
if (this.callbacks[eventName] && callback) {
const index = this.callbacks[eventName].indexOf(callback)
if (index > -1) {
this.callbacks[eventName].splice(index, 1)
if (this.callbacks[eventName] && callbackOrTag) {
if (typeof callbackOrTag === 'function') {
const index = this.callbacks[eventName].indexOf(callbackOrTag)
if (index > -1) {
this.callbacks[eventName].splice(index, 1)
}
} else {
let index = this.callbacks[eventName].length
while (index--) {
if (
this.callbacks[eventName][index].__EVENT_BUS_TAG__ != null &&
this.callbacks[eventName][index].__EVENT_BUS_TAG__ === callbackOrTag
) {
this.callbacks[eventName].splice(index, 1)
}
}
}
} else {
delete this.callbacks[eventName]
Expand All @@ -83,16 +140,40 @@ export class EventBus<TEvents extends Record<string, (...args: any[]) => any>> {
/**
* 发布事件。
*
* @param eventName 事件名称
* @param eventNameAndContext 事件名称和上下文
* @param args 传给事件回调的参数
* @returns 返回各事件回调的返回结果组成的数组
*/
emit<TName extends keyof TEvents>(
eventName: TName,
...args: Parameters<TEvents[TName]>
): Array<ReturnType<TEvents[TName]>> {
return (this.callbacks[eventName] || []).map(callback => {
return callback(...args)
emit<TListenerName extends keyof TListeners>(
// @ts-ignore
eventName: TListenerName | EventBusListenerDescriptor<TListenerName>,
...args: Parameters<TListeners[TListenerName]>
): Array<ReturnType<TListeners[TListenerName]>> {
const {
name,
context,
tag,
}: {
name: TListenerName
context?: any
tag?: string | number
} =
typeof eventName === 'object'
? eventName
: {
name: eventName,
}
let callbacks = this.callbacks[name] || []
if (tag != null) {
callbacks = callbacks.filter(
callback =>
callback.__EVENT_BUS_TAG__ != null &&
tag === callback.__EVENT_BUS_TAG__,
)
}
this.options?.beforeEmit?.[name]?.call(this, context)
return callbacks.map(callback => {
return callback.call(context, ...args)
})
}
}

0 comments on commit ed28ce6

Please sign in to comment.