Skip to content

Instantly share code, notes, and snippets.

@Misaka-0x447f
Last active June 4, 2025 12:46
Show Gist options
  • Save Misaka-0x447f/0c37018ae7bd944cbff54d27b6d4fd9f to your computer and use it in GitHub Desktop.
Save Misaka-0x447f/0c37018ae7bd944cbff54d27b6d4fd9f to your computer and use it in GitHub Desktop.
(此 gist 已进化为 npm 包,请前往仓库查看:https://github.com/Misaka-0x447f/createTypedEvent )
/**
* A modern eventManager, but typed to prevent errors.
* 更现代的事件管理器,同时具备内建 payload 类型机制以减少错误和帮助自动填充。
* @example
* type Payload = {ready: boolean}
* const networkStateChange = createTypedEvent<Payload>()
* const handler = (payload: Payload) => console.log(payload)
* networkStateChange.sub(handler)
* networkStateChange.dispatch({ready: true})
* networkStateChange.unsub(handler)
* @example
* const misakaStateChange = createTypedEvent<{selfDestructionInProgress: boolean}>()
* const unsub = createTypedEvent.sub(console.log) // returns unsub function without define handler outside.
* unsub()
* @example
* export const eventBus = {
* alice: createTypedEvent(),
* bob: createTypedEvent<{isE2eEncryption: boolean}>()
* }
* eventBus.bob.dispatch({isE2eEncryption: true})
*
* @member sub Subscribe to event. Returns an unsub method that does not require original callback.
* @member unsub Unsubscribe to event. Require original callback.
* @member dispatch Simply dispatch payload to every subscriber.
* @member once Only subscribe once.
* @member value Get the latest value.
*
* @param dispatchLastValueOnSubscribe If true, dispatch last value to new subscriber.
*
* @member sub 订阅事件。返回一个取消订阅方法,以便在不需要原始回调函数的情况下取消订阅。
* @member unsub 取消订阅事件。需要原始回调。
* @member dispatch 仅将 payload 分发给每个订阅者。
* @member once 仅订阅一次。
* @member value 获取最新的值。
*/
import { useMemo } from 'react';
import { useBeforeMount } from '@/utils/hooks';
import { useHybridState } from '@/utils/useHybridState';
import { logRuntimeError } from '@/utils/telemetry';
type cb<T> = (payload: T) => void;
export const createTypedEvent = <T = void>({
dispatchLastValueOnSubscribe = false,
initialValue,
}: {
dispatchLastValueOnSubscribe?: boolean;
initialValue?: T;
} = {}) => {
const history: T[] = initialValue ? [initialValue] : [];
const cbs: Array<cb<T>> = [];
const instance = {
sub: (cb: cb<T>) => {
cbs.push(cb);
if (dispatchLastValueOnSubscribe && history.length > 0) cb(history[0]);
return () => instance.unsub(cb);
},
unsub: (cb: cb<T>) => {
const index = cbs.indexOf(cb);
if (index === -1) return;
cbs.splice(index, 1);
},
dispatch: (payload: T) => {
cbs.map(v => v(payload));
history[0] = payload;
},
once: (cb: cb<T>) => {
instance.sub((arg: T) => {
cb(arg);
instance.unsub(cb);
});
return () => instance.unsub(cb);
},
get value() {
return history[0];
},
set value(_) {
logRuntimeError('createTypedEvent.value is read-only.');
},
};
return instance;
};
export const createTypedEventMemorized: typeof createTypedEvent = (...args) =>
useMemo(() => createTypedEvent(...args), []);
/**
* @description
* 获取 TypedEvent 事件管线中的最新 value。例如以下用法可以获得最新的市场价格:
* @example
* const [marketPrice] = useTypedEventValue(marketPriceUpdateEvent);
*/
export const useTypedEventValue = <T = void>(event: TypedEvent<T>) => {
const [value, setValue, valueRef] = useHybridState<T>(event.value);
useBeforeMount(() => {
return event.sub(setValue);
});
return [value, event.dispatch, valueRef] as const;
};
export type TypedEvent<T = void> = ReturnType<typeof createTypedEvent<T>>;
/**
* An eventManager, but typed to prevent errors.
* @example
* type Payload = {ready: boolean}
* const networkStateChange = new TypedEvent<Payload>()
* const handler = (payload: Payload) => console.log(payload)
* networkStateChange.sub(handler)
* networkStateChange.dispatch({ready: true})
* networkStateChange.unsub(handler)
* @example
* const misakaStateChange = new TypedEvent<{selfDestructionInProgress: boolean}>()
* const unsub = misakaStateChange.sub(console.log) // returns unsub function without define handler outside.
* unsub()
* @example
* export const eventBus = {
* alice: new TypedEvent(),
* bob: new TypedEvent<{isE2eEncryption: boolean}>()
* }
* eventBus.bob.dispatch({isE2eEncryption: true})
*
* @class TypedEvent
* @member sub Subscribe to event. Returns an unsub method that does not require original callback.
* @member unsub Unsubscribe to event. Require original callback.
* @member dispatch Simply dispatch payload to every subscriber.
* @member once Only subscribe once.
*/
type cb<T> = (payload: T) => void;
export class TypedEvent<T = void> {
constructor(dispatchLastValueOnSubscribe?: boolean) {
this.enableHistory = !!dispatchLastValueOnSubscribe;
}
private readonly enableHistory: boolean = false;
private history: T[] = [];
private cbs: Array<cb<T>> = [];
public sub(cb: cb<T>) {
this.cbs.push(cb);
if (this.enableHistory && this.history.length > 0) cb(this.history[0]);
return () => this.unsub(cb);
}
public unsub(cb: cb<T>) {
const index = this.cbs.indexOf(cb);
if (index === -1) return;
this.cbs.splice(index, 1);
}
public dispatch(payload: T) {
this.cbs.map(v => v(payload));
if (this.enableHistory) this.history = [payload];
}
public once(cb: cb<T>) {
this.sub((arg: T) => {
cb(arg);
this.unsub(cb);
});
}
public get lastValue() {
return history[0];
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment