@sttot/emitter
v1.0.4
Published
能够创建可以发送、监听事件的对象。实现 PubSub 模式或观察者模式。
Downloads
1
Readme
Emitter 事件系统
能够创建可以发送、监听事件的对象。实现 PubSub 模式或观察者模式。
简单使用
emitter 有一些全局的方法(属于全局事件对象),可以在不做任何配置的情况下简单使用事件系统。
可以携带事件负载(事件的额外信息)或者不带:
import { emit } from '@sttot/emitter';
emit('Event1'); // 不带事件负载
emit('Event2', { value: 1 }); // 携带负载
emit<{ value: number }>('Event2', { value: 2 }); // 可以对负载的类型进行指定
import { on } from '@sttot/emitter';
// 无负载,第一参数是事件名称,第二个参数是回调函数
on('Event1', () => console.log('Event1 emitted!'));
// 有负载 + 指定类型
on<{ value: number }>('Event2', ({ value }) =>
console.log('Event2 emitted!', value),
);
// 监听所有事件,回调函数的第一个参数是负载,第二个参数是事件名称
on('*', (payload, type) => console.log(payload, type));
once 和 on 一样,只是监听回调会在被触发过一次后自动销毁,即「只监听一次事件」:
once('Event1', () => {
/* 只会执行一次 */
});
once 和 on 都会返回一个函数,调用该函数会注销刚才注册的事件监听回调函数:
const unregister = on('Event1', () => {
/* */
});
unregister(); // 注销刚才的事件监听回调函数
这在 React 函数式编程中更好使用:
// 返回的注销函数正好作为 useEffect 生命周期结束的处理函数
// 如下代码可以做到在组件初始化时注册回调,在组件销毁时注销回调
React.useEffect(
() =>
on('Event', () => {
/* */
}),
[],
);
同时,on
和 once
还拥有 listeners
属性,保存了各自的所有注册的回调函数:
on.listeners.get('Event1'); // 获取所有对 Event1 事件的回调函数
on 和 once 的回调函数推荐使用箭头函数 () => { ... }
,因为这样可以尽可能避免出现 this
丢失等问题。但是如果确实需要将某个类方法作为回调函数(例如在 Cocos 的组件中,或者 React 的类组件中使用)时,可以采用如下的方法:
方法一:
// 假设 aClassInstance 是一个类实例,aFunction 是其中的一个方法
on('Event1', payload => aClassInstance.aFunction(payload));
方法二:
// 第三个参数是回调函数中 this 指向的对象,必须指定,否则会产生问题
on('Event1', aClassInstance.aFunction, aClassInstance);
举一个在 Cocos 中使用的例子:
@ccclass('GameController')
class GameController extends Component {
onEnable() {
on('GameStart', this.onGameStart, this);
}
onGameStart({ mapId }: { mapId: string }) {
//
}
}
这一般适用于 Cocos 组件等注册与注销不能写在一起的情况:
import { off } from '@sttot/emitter';
@ccclass('GameController')
class GameController extends Component {
onEnable() {
// 注册
on('GameStart', this.onGameStart, this);
once('AnotherEvent', this.anotherListener, this);
}
onGameStart({ mapId }: { mapId: string }) {
//
}
anotherListener() {}
onDisable() {
// 注销
off('GameStart', this.onGameStart, this);
// 注意 once 需要加第四个参数 true 来注销, false 或者不填则是注销 on
off('AnotherEvent', this.anotherListener, this, true);
}
}
其他用法:
// 什么都不填,会清空所有 on 和 once 注册的事件监听回调函数
off();
// 只填第一个参数,会注销所有通过 on 注册的对应事件的回调函数,注销 once 需要将第四个参数设为 true
off('Event1');
off('Event1', undefined, undefined, true);
// 填第一个和第二个参数,会注销所有通过 on 注册的对应事件的对应回调函数
off('Event1', aClassInstance.aFunction);
off('Event1', aClassInstance.aFunction, undefined, true);
// 填前三个参数,会注销通过 on 注册的对应事件、对应观察主题(就是 on 的第三个参数)对应的回调函数,主要是为了避免多个实例共同监听、注销时相互影响的情况
off('Event', aClassInstance.aFunction, aClassInstance);
off('Event', aClassInstance.aFunction, aClassInstance, true);
当我们在使用类实例的方法作为回调函数时,我们需要将类实例作为 on 或者 once 的第三个参数传入。这实际上是相当于,当前监听这个事件的主体(target),就是该类实例,即该第三个参数:
on('XXX', a.func, a);
这里的 a 可以理解成是观察事件 XXX 的主体。类似的,该主体还可能会同时观察其他事件,这是一个 Cocos 组件的典型用法。那么,我们进一步希望在这个主体的生命周期结束时(被销毁,或者被禁用时),能够比较方便的注销所有其监听过的事件,那么可以用如下的方法:
import { targetOff, on } from '@sttot/emitter';
@ccclass('GameController')
class GameController extends Component {
onEnable() {
// 对若干事件发起了监听
on('A', this.onA, this);
on('B', this.onB, this);
on('C', this.onC, this);
on('D', this.onD, this);
}
onDisable() {
// 全部注销
targetOff(this);
}
}
我们会遇到如下场景:一些顺序执行的动作,需要等待一些事件被触发后才激活后续的步骤。这样的代码如果使用 once 实现会出现嵌套地狱:
doSomething1();
once('A Ready', () => {
doSomething2();
once('B Ready', () => {
doSomething3();
once('C Ready', () => {
...
});
});
});
因此可以使用 waitFor 来解决(需要在异步函数中使用):
doSomething1();
await waitFor('A Ready');
doSomething2();
await waitFor('B Ready');
doSomething3();
await waitFor('C Ready');
...
进阶使用
如果直接使用全局事件对象的方法,往往不能很好的进行事件的负载类型推断,每次 emit、on 或者 once 都需要自己指定类型。如果需要类型推断和检验,或者需要多个相互独立的事件系统,可以使用 emitter 创建自己的事件系统对象:
import emitter from '@sttot/emitter';
// 对所有事件名称及其负载类型的定义
interface MyEvents {
Event1: void;
Event2: { value: number };
Event3: number;
Event4: IEvent4Payload;
}
// 定义一个自己的事件系统对象
export const myEmitter = emitter<MyEvents>();
其他文件可以引入这个自定义的事件系统对象,并获得对应的事件和负载类型提示:
import { myEmitter } from './xxx.ts';
// 如果安装了相应的插件,会对事件名称进行补全,
// 会对负载类型自动推断、验证
myEmitter.on('Event2', ({ value }) => { /* */ });