npm package: @sttot/emitter
Description: 能够创建可以发送、监听事件的对象。实现 PubSub 模式或观察者模式。
Latest version: 1.0.4

# Emitter 事件系统

能够创建可以发送、监听事件的对象。实现 PubSub 模式或观察者模式。

## 简单使用

emitter 有一些全局的方法（属于全局事件对象），可以在不做任何配置的情况下简单使用事件系统。

<details>
<summary>发射一个事件</summary>

可以携带事件负载(事件的额外信息)或者不带：

```ts
import { emit } from '@sttot/emitter';

emit('Event1'); // 不带事件负载
emit('Event2', { value: 1 }); // 携带负载
emit<{ value: number }>('Event2', { value: 2 }); // 可以对负载的类型进行指定
```

</details>

<details>
<summary>监听所发射的事件</summary>

```ts
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 一样，只是监听回调会在被触发过一次后自动销毁，即「只监听一次事件」：

```ts
once('Event1', () => {
  /* 只会执行一次 */
});
```

once 和 on 都会返回一个函数，调用该函数会注销刚才注册的事件监听回调函数：

```ts
const unregister = on('Event1', () => {
  /* */
});
unregister(); // 注销刚才的事件监听回调函数
```

这在 React 函数式编程中更好使用：

```ts
// 返回的注销函数正好作为 useEffect 生命周期结束的处理函数
// 如下代码可以做到在组件初始化时注册回调，在组件销毁时注销回调
React.useEffect(
  () =>
    on('Event', () => {
      /* */
    }),
  [],
);
```

同时，`on` 和 `once` 还拥有 `listeners` 属性，保存了各自的所有注册的回调函数：

```ts
on.listeners.get('Event1'); // 获取所有对 Event1 事件的回调函数
```

on 和 once 的回调函数推荐使用箭头函数 `() => { ... }`，因为这样可以尽可能避免出现 `this` 丢失等问题。但是如果确实需要将某个类方法作为回调函数(例如在 Cocos 的组件中，或者 React 的类组件中使用)时，可以采用如下的方法：

方法一：

```ts
// 假设 aClassInstance 是一个类实例，aFunction 是其中的一个方法
on('Event1', payload => aClassInstance.aFunction(payload));
```

方法二：

```ts
// 第三个参数是回调函数中 this 指向的对象，必须指定，否则会产生问题
on('Event1', aClassInstance.aFunction, aClassInstance);
```

举一个在 Cocos 中使用的例子：

```ts
@ccclass('GameController')
class GameController extends Component {
  onEnable() {
    on('GameStart', this.onGameStart, this);
  }
  onGameStart({ mapId }: { mapId: string }) {
    //
  }
}
```

</details>

<details>
<summary>也可以使用 off 来注销回调函数</summary>

这一般适用于 Cocos 组件等注册与注销不能写在一起的情况：

```ts
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);
  }
}
```

其他用法：

```ts
// 什么都不填，会清空所有 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);
```

</details>

<details>
<summary>targetOff 注销某个观察实体的所有事件回调</summary>

当我们在使用类实例的方法作为回调函数时，我们需要将类实例作为 on 或者 once 的第三个参数传入。这实际上是相当于，当前监听这个事件的主体(target)，就是该类实例，即该第三个参数：

```ts
on('XXX', a.func, a);
```

这里的 a 可以理解成是观察事件 XXX 的主体。类似的，该主体还可能会同时观察其他事件，这是一个 Cocos 组件的典型用法。那么，我们进一步希望在这个主体的生命周期结束时(被销毁，或者被禁用时)，能够比较方便的注销所有其监听过的事件，那么可以用如下的方法：

```ts
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);
  }
}
```

</details>

<details>
<summary>wairFor 异步事件阻塞</summary>

我们会遇到如下场景：一些顺序执行的动作，需要等待一些事件被触发后才激活后续的步骤。这样的代码如果使用 once 实现会出现嵌套地狱：

```ts
doSomething1();
once('A Ready', () => {
  doSomething2();
  once('B Ready', () => {
    doSomething3();
    once('C Ready', () => {
      ...
    });
  });
});
```

因此可以使用 waitFor 来解决(需要在异步函数中使用)：

```ts
doSomething1();
await waitFor('A Ready');
doSomething2();
await waitFor('B Ready');
doSomething3();
await waitFor('C Ready');
...
```

</details>

---

## 进阶使用

如果直接使用全局事件对象的方法，往往不能很好的进行事件的负载类型推断，每次 emit、on 或者 once 都需要自己指定类型。如果需要类型推断和检验，或者需要多个相互独立的事件系统，可以使用 emitter 创建自己的事件系统对象：

```ts
import emitter from '@sttot/emitter';

// 对所有事件名称及其负载类型的定义
interface MyEvents {
  Event1: void;
  Event2: { value: number };
  Event3: number;
  Event4: IEvent4Payload;
}

// 定义一个自己的事件系统对象
export const myEmitter = emitter<MyEvents>();
```

其他文件可以引入这个自定义的事件系统对象，并获得对应的事件和负载类型提示：

```ts
import { myEmitter } from './xxx.ts';

// 如果安装了相应的插件，会对事件名称进行补全，
// 会对负载类型自动推断、验证
myEmitter.on('Event2', ({ value }) => { /* */ });
```