Vue3+Pixi.js:类属性/重写父类的DisplayObject事件监听失效
Vue3 + Pixi.js + TypeScript OOP开发中的两类问题
我在Vue3中基于Pixi.js开发项目,因为多数图形模式相似但行为、属性不同,采用TypeScript面向对象(OOP)方式避免代码重复,却遇到以下两个核心问题:
- 当DisplayObject作为类属性创建或从父类重写时,事件监听器无法正常工作,触发错误
cannot find propagation path to disconnected target; - 修改DisplayObject的属性后,画面不会触发重渲染。
相关代码实现
主组件(Vue)
<script setup lang="ts"> const main = ref<HTMLElement | null>(null) let visualisationEngine: VisualisationEngine onMounted(() => { if (main.value) { visualisationEngine = new VisualisationEngine(main.value!!, window) visualisationEngine.init() } }) </script>
VisualisationEngine.ts
import { Application, Graphics } from 'pixi.js' import '@pixi/unsafe-eval' // Electron环境下使用 import { onEscKeyDown, onPointerDown, onPointerMove, onPointerUp } from '@/visualisation_engine/eventHandlers' import { Viewport } from 'pixi-viewport' interface GlobalThis { __PIXI_APP__: Application<HTMLCanvasElement> } export class VisualisationEngine { app: Application<HTMLCanvasElement> elem: HTMLElement window: Window viewport: Viewport constructor(elem: HTMLElement, window: Window) { this.window = window this.app = new Application<HTMLCanvasElement>({ antialias: true, autoDensity: true, resizeTo: elem, hello: true }) this.viewport = new Viewport({ // 省略配置 }) this.elem = elem ;(globalThis as any as GlobalThis).__PIXI_APP__ = this.app // 用于Pixi调试工具 } init() { this.render() const background = this.drawBackground() // 临时用局部变量,类属性绑定事件会失效 this.startEventHandlers(background) } render() { this.elem.appendChild(this.app.view) this.app.stage.addChild(this.viewport) } drawBackground() { const background = new Graphics() background.beginFill(0x000000) background.drawRect(0, 0, this.elem.clientWidth, this.elem.clientHeight) background.endFill() background.eventMode = 'static' this.viewport.addChild(background) return background } startEventHandlers(graphic: Graphics) { document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { onEscKeyDown() } }) graphic .on('pointerdown', (event) => { onPointerDown(event, (elem) => this.viewport.addChild(elem)) }) .on('pointermove', (event) => { onPointerMove(event, (elem) => this.viewport.addChild(elem)) }) .on('pointerup', (event) => onPointerUp(event)) } }
eventHandlers.ts
export const onPointerDown = (event: FederatedPointerEvent, callback: (elem: Graphics) => void) => { const mainStore = useMainStore() if (mainStore.elementToAdd !== null) { mainStore.elementToAdd.position.set(Math.round(event.globalX), Math.round(event.globalY)) const elem = mainStore.elementToAdd.draw() // 临时返回Graphics,直接返回Container会触发事件错误 if (callback) { callback(elem) } mainStore.onScreenElements.push(mainStore.elementToAdd) mainStore.elementToAdd = null return } // 省略其他逻辑 mainStore.currentlySelectedElement = null } export const onPointerUp = (event: FederatedPointerEvent) => { const mainStore = useMainStore() if (useMainStore().draggingElement !== null) { useMainStore().draggingElement = null } } export const onPointerMove = (event: FederatedPointerEvent, callback: (elem: Graphics) => void) => { if (useMainStore().draggingElement !== null) { useMainStore().draggingElement!!.move(event.globalX, event.globalY) // 位置已变更,但画面无更新 console.log(useMainStore().draggingElement!!.position) // 控制台显示坐标正确 // 调用element.parent会返回null,对象已脱离渲染树 } }
BaseElement.ts
import { defineStore } from 'pinia' import { Container, Graphics } from 'pixi.js' import { useMainStore } from '@/store/main' import { ref } from 'vue' export abstract class BaseElement extends Container { useStore nodes: number[] abstract readonly type: string id: number = Math.floor(Math.random() * 1000000) readonly store = defineStore(`${this.constructor.name}-${this.id}`, () => { const name = ref(this.name) const value = ref<{ [key: string]: { [key: string]: string } }>({}) return { name, value } }) protected constructor(x: number = 0, y: number = 0, name: string, nodes: number[]) { super() this.nodes = nodes this.x = x this.y = y this.useStore = this.store() this.useStore.$patch({ name: name }) } abstract draw(): Graphics initializeEventListeners(graphic: Graphics) { graphic.hitArea = graphic.getBounds() graphic.eventMode = 'static' graphic.on( 'pointerdown', () => { if (useMainStore().currentAction === 'move') { useMainStore().draggingElement = this } else { useMainStore().currentlySelectedElement = this } }, this ) graphic.on('pointerup', () => { console.log('pointer up') }) } move(x: number, y: number) { this.x = x this.y = y } }
Resistor.ts(示例元素)
import { BaseElement } from './BaseElement' import { Graphics, Point } from 'pixi.js' export class Resistor extends BaseElement { override type = 'Resistor' constructor(x: number, y: number, nodes: [number, number], name: string = 'R', resistance: number) { super(x, y, name, nodes) } override draw() { // 原本计划将图形作为父类属性,但会导致事件完全失效 const resistorGraphic = new Graphics() const body = new Graphics() resistorGraphic .lineStyle(DIMENSIONS.CONDUCTOR_WIDTH, COLORS.CONDUCTOR) .moveTo(this.x, this.y) // 省略绘制逻辑 body.addChild(resistorGraphic) this.addConductor().forEach((conductor) => { body.addChild(conductor) }) this.initializeEventListeners(resistorGraphic) return body } }
元素添加逻辑(Pinia)
// 用户选择元素后执行 mainStore.setElementToAdd(new Resistor(...)) // Store中的方法 setElementToAdd(element: BaseElement) { // 省略状态更新逻辑 state.elementToAdd = element }
问题修复方案
问题1:事件监听器失效与传播路径错误
核心原因
- DisplayObject未正确加入PIXI渲染树,事件系统无法找到传播路径;
BaseElement作为Container,未将draw()创建的Graphics添加到自身,导致图形与容器脱节;- 事件绑定时机过早,对象未挂载到viewport就绑定事件。
修复步骤
让BaseElement容器包含自身图形
修改BaseElement.ts,将图形作为类属性存储并添加到容器:export abstract class BaseElement extends Container { protected graphic: Graphics | null = null; // 省略其他属性 abstract draw(): void; // 改为无返回值,直接将图形添加到自身容器 initializeEventListeners() { if (this.graphic) { this.graphic.eventMode = 'static'; // 延迟设置hitArea,确保图形已渲染 requestAnimationFrame(() => { this.graphic!.hitArea = this.graphic!.getBounds(); }); this.graphic.on('pointerdown', () => { const mainStore = useMainStore(); if (mainStore.currentAction === 'move') { mainStore.draggingElement = this; } else { mainStore.currentlySelectedElement = this; } }, this); } // 给容器本身设置事件模式,确保事件能正确传播 this.eventMode = 'static'; } }修改
Resistor.ts的draw()方法:override draw() { const resistorGraphic = new Graphics(); const body = new Graphics(); // 省略绘制逻辑 body.addChild(resistorGraphic); this.addConductor().forEach(conductor => body.addChild(conductor)); this.graphic = body; this.addChild(body); // 将图形添加到当前Container this.initializeEventListeners(); }确保事件绑定前对象已加入渲染树
修改VisualisationEngine.ts,将background改为类属性,先添加到viewport再绑定事件:export class VisualisationEngine { background: Graphics | null = null; // 省略其他属性 init() { this.render(); this.drawBackground(); // 先创建并添加到viewport this.startEventHandlers(); // 再绑定事件 } drawBackground() { this.background = new Graphics(); this.background.beginFill(0x000000); this.background.drawRect(0, 0, this.elem.clientWidth, this.elem.clientHeight); this.background.endFill(); this.background.eventMode = 'static'; this.viewport.addChild(this.background); } startEventHandlers() { document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { onEscKeyDown(); } }); if (this.background) { this.background .on('pointerdown', (event) => { onPointerDown(event, (elem) => this.viewport.addChild(elem)); }) .on('pointermove', (event) => { onPointerMove(event, (elem) => this.viewport.addChild(elem)); }) .on('pointerup', (event) => onPointerUp(event)); } } }
问题2:修改属性后画面不重渲染
核心原因
BaseElement实例未被添加到viewport,仅添加了draw()返回的Graphics,导致容器与渲染图形脱节,修改容器坐标无法影响画面;hitArea设置过早,图形未渲染就获取边界,导致事件触发区域错误。
修复步骤
将BaseElement实例添加到viewport
修改eventHandlers.ts的onPointerDown方法,直接添加BaseElement容器:export const onPointerDown = (event: FederatedPointerEvent, callback: (elem: Container) => void) => { const mainStore = useMainStore(); if (mainStore.elementToAdd !== null) { mainStore.elementToAdd.position.set(Math.round(event.globalX), Math.round(event.globalY)); mainStore.elementToAdd.draw(); // 先执行draw,将图形添加到容器 callback(mainStore.elementToAdd); // 添加容器到viewport mainStore.onScreenElements.push(mainStore.elementToAdd); mainStore.elementToAdd = null; return; } // 省略其他逻辑 }同步修改
VisualisationEngine.ts中回调的参数类型:graphic.on('pointerdown', (event) => { onPointerDown(event, (elem) => this.viewport.addChild(elem)); // elem为Container类型 })确保渲染更新的正确性
只要BaseElement实例在PIXI渲染树中,修改x/y属性会自动触发重绘。如果仍无更新,可以在move()方法中手动标记对象为需要更新:move(x: number, y: number) { this.x = x; this.y = y; this.dirty = true; // 标记为脏,强制PIXI更新 }
内容的提问来源于stack exchange,提问作者ayitinya
相关产品推荐
相关产品推荐

