PixiJS 源码解读:Runner 事件通知类

大家好,我是前端西瓜哥。

PixiJS 的 Runner 类是高性能的事件通知类。其实就是一个简易的发布订阅库。

发布订阅库,我们比较熟悉的就是 Nodejs 的 EventEmitter。

不过这个 Runner 的逻辑稍微有点特殊,后面会说它怎么特殊。

使用示例

typescript 复制代码
import { Runner } from "@pixi/runner";

const loadedRunner = new Runner("loaded");

const listener = {
  loaded(n1: number, n2: number) {
    console.log("前端西瓜哥", n1, n2);
  }
};

loadedRunner.add(listener);

loadedRunner.emit(1, 2); // 输出:前端西瓜哥 1 2

首先通过 new Runner(name) 创建一个 Runner 实例,这里需要传入一个字符串类型的 name。

之后通过 runner.add 方法添加一个监听器对象 listener。

最后通过 runner.emit 方法触发事件,之前绑定的监听器的 listener[name] 方法会被执行。

和我们熟悉的 Nodejs 的 EventEmitter 不一样,有一些特别的点:

  1. 一个 Runner 只能绑定一个事件,不像 EventEmitter 的 on 方法,还能多指定一个事件名。

  2. 绑定的监听器是一个对象,并会在触发事件时调用 Runner 初始化时设置的 name 对应的函数。这样做的优点是监听器执行时 this 不会丢失。EventEmitter 绑定的直接就是一个函数。

然后它和 EventEmitter 一样,是类型不安全的:emit 传的参数并没有限定。

类型安全的写法:

用 TypeScript 实现类型安全的 EventEmitter,这下不用怕写错事件名了

源码解读

构造函数

首先是构造函数。

typescript 复制代码
export class Runner {
  public items: any[];
  private _name: string;
  private _aliasCount: number;

  constructor(name: string) {
    this.items = [];
    this._name = name;
    this._aliasCount = 0;
  }

  // ...
}

简单的初始化操作,这个 name 我们要保存下来,之后我们执行监听器对象,需要这个 name 作为 key 去找到方法去执行。

items 是保存监听器对象的数组。

_aliasCount 是一个标识,标识是否在 emit(触发事件)阶段,用于防止 emit 时改变了 items,导致不可预期的行为。

添加监听器

然后是 add 方法,用于添加监听器。

kotlin 复制代码
public add(item: unknown): this {
  if ((item as any)[this._name]) {
    this.ensureNonAliasedItems();
    this.remove(item); // 如果存在,先删除
    this.items.push(item); // 添加的末尾
  }

  return this;
}

监听器对象必须有对应的 key 才能被添加进去。

为了保证 this.items 不出现多个相同的对象,会将其删除。然后把监听器对象放到 this.items 末尾。

返回 this,是为了实现链式调用。

this.ensureNonAliasedItems() 方法用于处理一些特殊 case。

比如在 emit 阶段发生了 add 操作,PixiJS 会防止其在本轮 emit 被执行,为此会拷贝一份新的 items。

kotlin 复制代码
private ensureNonAliasedItems(): void {
  if (this._aliasCount > 0 && this.items.length > 1) {
    this._aliasCount = 0;
    this.items = this.items.slice(0);
  }
}

事件触发

emit 会触发事件,别名有 dispatch、run。

typescript 复制代码
 public emit(
  a0?: unknown,
  a1?: unknown,
  a2?: unknown,
  a3?: unknown,
  a4?: unknown,
  a5?: unknown,
  a6?: unknown,
  a7?: unknown
): this {
  if (arguments.length > 8) {
    throw new Error('max arguments reached');
  }

  const { name, items } = this;
  this._aliasCount++;

  for (let i = 0, len = items.length; i < len; i++) {
    items[i][name](a0, a1, a2, a3, a4, a5, a6, a7);
  }

  if (items === this.items) {
    this._aliasCount--;
  }
  return this;
}

核心逻辑:遍历 this.items 数组,顺序执行监听器的 key 为 this.name 的方法。

删除监听器

remove,删除监听器。

kotlin 复制代码
public remove(item: unknown): this {
  const index = this.items.indexOf(item);

  if (index !== -1) {
    this.ensureNonAliasedItems();
    this.items.splice(index, 1);
  }

  return this;
}

其他方法

contains:查看指定对象是已经是被绑定为监听器。

arduino 复制代码
public contains(item: unknown): boolean {
  return this.items.includes(item);
}

removeAll:删除所有监听器.

kotlin 复制代码
public removeAll(): this {
  this.ensureNonAliasedItems();
  this.items.length = 0;
  return this;
}

destory:销毁。销毁后就不能再用了,否则会报错。

csharp 复制代码
public destroy(): void {
  this.removeAll();
  this.items = null;
  this._name = null;
}

empty,是一个 getter,判断是否存在监听器集合。PixiJS 确实喜欢用 getter

arduino 复制代码
public get empty(): boolean {
  return this.items.length === 0;
}

结尾

通常我们会在 PixiJS 的类中看到名为 disposeRunner 的成员属性,说明这个类会通过事件订阅的方式和其他模块通信。

发布订阅库我实在是分析得够多了,基本的套路就 3 个:把监听器函数放到有序表中,触发事件时顺序调用,支持删除监听器(3 种风格)。

PixiJS 的 Runner 功能并不多,其中特殊的调用逻辑(调用监听器的特定 key)显然是用于 PixiJS 内部模块的风格。

我是前端西瓜哥,欢迎关注我,学习更多 PixiJS 知识。


相关阅读,

PixiJS 源码深度解读:用于循环渲染的 Ticker 模块

用 TypeScript 实现类型安全的 EventEmitter,这下不用怕写错事件名了

事件订阅的几种实现风格

类型体操:探究 TypeScript 内置高级类型

相关推荐
代码匠心8 分钟前
AI 自动编程:一句话设计高颜值博客
前端·ai·ai编程·claude
_AaronWong1 小时前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
cxxcode1 小时前
I/O 多路复用:从浏览器到 Linux 内核
前端
用户5433081441941 小时前
AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例
前端
JarvanMo2 小时前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
恋猫de小郭2 小时前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木2 小时前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮2 小时前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati2 小时前
Vue3 父子组件通信完全指南
前端·面试
是一碗螺丝粉2 小时前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain