PixiJS 源碼解讀:Runner 事件通知類
大家好,我是前端西瓜哥。
PixiJS 的 Runner 類是高性能的事件通知類。其實就是一個簡易的發布訂閱庫。
發布訂閱庫,我們比較熟悉的就是 Nodejs 的 EventEmitter。
不過這個 Runner 的邏輯稍微有點特殊,后面會說它怎么特殊。
使用示例
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 不一樣,有一些特別的點:
- 一個 Runner 只能綁定一個事件,不像 EventEmitter 的 on 方法,還能多指定一個事件名。
- 綁定的監聽器是一個對象,并會在觸發事件時調用 Runner 初始化時設置的 name 對應的函數。這樣做的優點是監聽器執行時 this 不會丟失。EventEmitter 綁定的直接就是一個函數。
然后它和 EventEmitter 一樣,是類型不安全的:emit 傳的參數并沒有限定。
源碼解讀
構造函數
首先是構造函數。
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 方法,用于添加監聽器。
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。
private ensureNonAliasedItems(): void {
if (this._aliasCount > 0 && this.items.length > 1) {
this._aliasCount = 0;
this.items = this.items.slice(0);
}
}
事件觸發
emit 會觸發事件,別名有 dispatch、run。
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,刪除監聽器。
public remove(item: unknown): this {
const index = this.items.indexOf(item);
if (index !== -1) {
this.ensureNonAliasedItems();
this.items.splice(index, 1);
}
return this;
}
其他方法
contains:查看指定對象是已經是被綁定為監聽器。
public contains(item: unknown): boolean {
return this.items.includes(item);
}
removeAll:刪除所有監聽器.
public removeAll(): this {
this.ensureNonAliasedItems();
this.items.length = 0;
return this;
}
destory:銷毀。銷毀后就不能再用了,否則會報錯。
public destroy(): void {
this.removeAll();
this.items = null;
this._name = null;
}
empty,是一個 getter,判斷是否存在監聽器集合。PixiJS 確實喜歡用 getter
public get empty(): boolean {
return this.items.length === 0;
}
結尾
通常我們會在 PixiJS 的類中看到名為 disposeRunner 的成員屬性,說明這個類會通過事件訂閱的方式和其他模塊通信。
發布訂閱庫我實在是分析得夠多了,基本的套路就 3 個:把監聽器函數放到有序表中,觸發事件時順序調用,支持刪除監聽器(3 種風格)。
PixiJS 的 Runner 功能并不多,其中特殊的調用邏輯(調用監聽器的特定 key)顯然是用于 PixiJS 內部模塊的風格。