PixiJS 源碼解讀:Runner 事件通知類
大家好,我是前端西瓜哥。
PixiJS 的 Runner 類是高性能的事件通知類。其實(shí)就是一個(gè)簡(jiǎn)易的發(fā)布訂閱庫(kù)。
發(fā)布訂閱庫(kù),我們比較熟悉的就是 Nodejs 的 EventEmitter。
不過(guò)這個(gè) Runner 的邏輯稍微有點(diǎn)特殊,后面會(huì)說(shuō)它怎么特殊。
使用示例
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
首先通過(guò) new Runner(name) 創(chuàng)建一個(gè) Runner 實(shí)例,這里需要傳入一個(gè)字符串類型的 name。
之后通過(guò) runner.add 方法添加一個(gè)監(jiān)聽器對(duì)象 listener。
最后通過(guò) runner.emit 方法觸發(fā)事件,之前綁定的監(jiān)聽器的 listener[name] 方法會(huì)被執(zhí)行。
和我們熟悉的 Nodejs 的 EventEmitter 不一樣,有一些特別的點(diǎn):
- 一個(gè) Runner 只能綁定一個(gè)事件,不像 EventEmitter 的 on 方法,還能多指定一個(gè)事件名。
- 綁定的監(jiān)聽器是一個(gè)對(duì)象,并會(huì)在觸發(fā)事件時(shí)調(diào)用 Runner 初始化時(shí)設(shè)置的 name 對(duì)應(yīng)的函數(shù)。這樣做的優(yōu)點(diǎn)是監(jiān)聽器執(zhí)行時(shí) this 不會(huì)丟失。EventEmitter 綁定的直接就是一個(gè)函數(shù)。
然后它和 EventEmitter 一樣,是類型不安全的:emit 傳的參數(shù)并沒有限定。
源碼解讀
構(gòu)造函數(shù)
首先是構(gòu)造函數(shù)。
export class Runner {
public items: any[];
private _name: string;
private _aliasCount: number;
constructor(name: string) {
this.items = [];
this._name = name;
this._aliasCount = 0;
}
// ...
}
簡(jiǎn)單的初始化操作,這個(gè) name 我們要保存下來(lái),之后我們執(zhí)行監(jiān)聽器對(duì)象,需要這個(gè) name 作為 key 去找到方法去執(zhí)行。
items 是保存監(jiān)聽器對(duì)象的數(shù)組。
_aliasCount 是一個(gè)標(biāo)識(shí),標(biāo)識(shí)是否在 emit(觸發(fā)事件)階段,用于防止 emit 時(shí)改變了 items,導(dǎo)致不可預(yù)期的行為。
添加監(jiān)聽器
然后是 add 方法,用于添加監(jiān)聽器。
public add(item: unknown): this {
if ((item as any)[this._name]) {
this.ensureNonAliasedItems();
this.remove(item); // 如果存在,先刪除
this.items.push(item); // 添加的末尾
}
return this;
}
監(jiān)聽器對(duì)象必須有對(duì)應(yīng)的 key 才能被添加進(jìn)去。
為了保證 this.items 不出現(xiàn)多個(gè)相同的對(duì)象,會(huì)將其刪除。然后把監(jiān)聽器對(duì)象放到 this.items 末尾。
返回 this,是為了實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用。
this.ensureNonAliasedItems() 方法用于處理一些特殊 case。
比如在 emit 階段發(fā)生了 add 操作,PixiJS 會(huì)防止其在本輪 emit 被執(zhí)行,為此會(huì)拷貝一份新的 items。
private ensureNonAliasedItems(): void {
if (this._aliasCount > 0 && this.items.length > 1) {
this._aliasCount = 0;
this.items = this.items.slice(0);
}
}
事件觸發(fā)
emit 會(huì)觸發(fā)事件,別名有 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 數(shù)組,順序執(zhí)行監(jiān)聽器的 key 為 this.name 的方法。
刪除監(jiān)聽器
remove,刪除監(jiān)聽器。
public remove(item: unknown): this {
const index = this.items.indexOf(item);
if (index !== -1) {
this.ensureNonAliasedItems();
this.items.splice(index, 1);
}
return this;
}
其他方法
contains:查看指定對(duì)象是已經(jīng)是被綁定為監(jiān)聽器。
public contains(item: unknown): boolean {
return this.items.includes(item);
}
removeAll:刪除所有監(jiān)聽器.
public removeAll(): this {
this.ensureNonAliasedItems();
this.items.length = 0;
return this;
}
destory:銷毀。銷毀后就不能再用了,否則會(huì)報(bào)錯(cuò)。
public destroy(): void {
this.removeAll();
this.items = null;
this._name = null;
}
empty,是一個(gè) getter,判斷是否存在監(jiān)聽器集合。PixiJS 確實(shí)喜歡用 getter
public get empty(): boolean {
return this.items.length === 0;
}
結(jié)尾
通常我們會(huì)在 PixiJS 的類中看到名為 disposeRunner 的成員屬性,說(shuō)明這個(gè)類會(huì)通過(guò)事件訂閱的方式和其他模塊通信。
發(fā)布訂閱庫(kù)我實(shí)在是分析得夠多了,基本的套路就 3 個(gè):把監(jiān)聽器函數(shù)放到有序表中,觸發(fā)事件時(shí)順序調(diào)用,支持刪除監(jiān)聽器(3 種風(fēng)格)。
PixiJS 的 Runner 功能并不多,其中特殊的調(diào)用邏輯(調(diào)用監(jiān)聽器的特定 key)顯然是用于 PixiJS 內(nèi)部模塊的風(fēng)格。