Skip to content

Instantly share code, notes, and snippets.

@bolasblack
Last active August 15, 2021 09:15
Show Gist options
  • Save bolasblack/2a2e481f9a1bb1ea7785be335035bcf9 to your computer and use it in GitHub Desktop.
Save bolasblack/2a2e481f9a1bb1ea7785be335035bcf9 to your computer and use it in GitHub Desktop.
type UnknownObject = Record<string, unknown>
/**
* 一个 EventEmitter ,当注册事件的对象(`gcTarget`)被 GC 时,会自动取消注册事件
*
* WARNING: 使用这个 EventEmitter 时需要注意,为了避免 `callback` 里可能会引用
* `gcTarget` 导致 `gcTarget` 本身不被回收,所以代码实现中对 `callback` 是持有的弱引用,
* 因此 `gcTarget` 本身需要持有 `callback` 的强引用
*
* @example
* ```
* const em = AutoUnlistenEventEmitter.create()
*
* class Xxx {
* constructor() {
* em.on(this, 'eventName', (data) => {
* console.log('eventData', data)
* })
* }
* }
*
* const xxx = new Xxx
* em.emit('eventName', 123)
* // 如果这么做的话,由于 `callback` 本身没有被任何地方引用,可能早就被回收了,所以
* // `em.emit` 可能不会生效,可能不会有任何 `console.log`(考虑到 GC 算法不一定会马上回
* // 收,所以只能说“可能”)
*
* // 正确做法是下面这样
*
* class Yyy {
* _onEvent = (data) => {
* console.log('eventData', data)
* }
* constructor() {
* em.on(this, 'eventName', this._onEvent)
* }
* }
*
* const yyy = new Yyy
* em.emit('eventName', 123)
* // 由于 `yyy` 本身持有 `_onEvent` 的引用,所以回调函数本身不会被 GC ,因此 `em.emit`
* // 后会打印出结果
* ```
*/
export interface SafeEventEmitter<EventMap extends UnknownObject> {
emit(event: keyof EventMap, data: EventMap[typeof event]): void
on(
// eslint-disable-next-line @typescript-eslint/ban-types
gcTarget: object,
event: keyof EventMap,
callback: (data: EventMap[typeof event]) => void,
): () => void
}
export namespace SafeEventEmitter {
export const create = <
EventMap extends UnknownObject
>(): SafeEventEmitter<EventMap> => {
type EventName = keyof EventMap
type Callback = (data: EventMap[EventName]) => void
type CallbackId = UnknownObject
const callbackIds: Partial<Record<EventName, CallbackId[]>> = {}
const callbacks = new WeakMap<CallbackId, Callback>()
const finRegistry = new FinalizationRegistry(
(heldValue: { event: EventName; callbackId: CallbackId }) => {
const ids = callbackIds[heldValue.event]
if (ids) {
callbackIds[heldValue.event] = ids.filter(
id => id !== heldValue.callbackId,
)
}
callbacks.delete(heldValue.callbackId)
},
)
return {
on(gcTarget: UnknownObject, event: EventName, callback: Callback) {
const callbackId: CallbackId = {}
callbackIds[event] ??= []
callbackIds[event]!.push(callbackId)
callbacks.set(callbackId, callback)
finRegistry.register(gcTarget, callbackId, callbackId)
return () => {
finRegistry.unregister(callbackId)
}
},
emit(event: EventName, eventData: any): void {
callbackIds[event]?.forEach(id => {
callbacks.get(id)?.(eventData)
})
},
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment