Last active
August 15, 2021 09:15
-
-
Save bolasblack/2a2e481f9a1bb1ea7785be335035bcf9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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