Motion Canvas 事件系统全攻略:EventDispatcher 与异步处理
Motion Canvas 作为一款基于代码的动画创作工具,其事件系统是实现交互逻辑和状态管理的核心组件。本文将深入解析 EventDispatcher(事件调度器)与异步事件处理机制,帮助开发者构建响应式动画场景。
事件系统核心架构
Motion Canvas 事件系统采用分层设计,核心类位于 packages/core/src/events/ 目录下,主要包含三大基础组件:
- EventDispatcherBase:抽象基类,定义事件订阅与通知的核心接口
- EventDispatcher:同步事件调度器,用于处理即时响应的事件流
- AsyncEventDispatcher:异步事件调度器,支持基于 Promise 的异步处理流程
THE 0TH POSITION OF THE ORIGINAL IMAGE
同步事件处理:EventDispatcher 实战
EventDispatcher 是处理同步事件的基础类,通过 dispatch 方法触发事件并立即通知所有订阅者。其核心实现位于 packages/core/src/events/EventDispatcher.ts,关键代码如下:
export class EventDispatcher<T> extends EventDispatcherBase<T> {
public dispatch(value: T) {
this.notifySubscribers(value);
}
}
基础用法:创建与订阅事件
创建自定义事件的标准流程包含三个步骤:定义事件调度器、暴露订阅接口、触发事件通知:
class AnimationPlayer {
// 创建私有事件调度器
private frameUpdated = new EventDispatcher<number>();
// 暴露订阅接口
public get onFrameUpdated(): SubscribableEvent<number> {
return this.frameUpdated.subscribable;
}
// 触发事件
private update(currentFrame: number) {
this.frameUpdated.dispatch(currentFrame);
}
}
// 使用示例
const player = new AnimationPlayer();
player.onFrameUpdated.subscribe(frame => {
console.log(`当前帧: ${frame}`);
});
事件生命周期管理
EventDispatcher 提供完整的订阅管理机制,支持单次订阅、取消订阅等高级操作:
// 单次订阅(自动取消)
player.onFrameUpdated.once(frame => {
console.log(`初始帧: ${frame}`);
});
// 带标识符的订阅
const subscription = player.onFrameUpdated.subscribe(frame => {
console.log(`带标识订阅: ${frame}`);
});
// 取消订阅
subscription.unsubscribe();
异步事件处理:AsyncEventDispatcher
对于需要异步处理的场景(如网络请求、文件读写),AsyncEventDispatcher 提供基于 Promise 的事件流控制,其定义位于 packages/core/src/events/AsyncEventDispatcher.ts。
核心特性
- 异步通知:所有订阅者函数返回 Promise
- 顺序执行:通过
Promise.all等待所有处理完成 - 类型安全:支持泛型参数的异步事件处理
基础实现
export class AsyncEventDispatcher<T> extends EventDispatcherBase<
T,
AsyncEventHandler<T>
> {
public async dispatch(value: T): Promise<void> {
await Promise.all(this.notifySubscribers(value));
}
}
异步事件应用场景
场景1:动画资源预加载
class AssetLoader {
private assetsLoaded = new AsyncEventDispatcher<string[]>();
public get onAssetsLoaded(): SubscribableAsyncEvent<string[]> {
return this.assetsLoaded.subscribable;
}
public async loadAssets(paths: string[]): Promise<void> {
const loadedAssets = await Promise.all(
paths.map(path => loadResource(path))
);
// 等待所有订阅者处理完成后再继续
await this.assetsLoaded.dispatch(loadedAssets);
console.log("所有资源处理完成");
}
}
场景2:多步骤动画序列
// 订阅异步事件
player.onAnimationComplete.subscribe(async (result) => {
await saveAnimationState(result);
await analytics.trackEvent("animation_completed");
});
// 调度异步事件
await player.playAnimation(sequence);
// 确保动画完成且所有后续处理完成
console.log("动画流程完全结束");
高级应用模式
事件转发与组合
通过事件转发可以实现复杂的事件流控制,例如将多个组件的事件汇总到统一的事件总线:
class EventBus {
private buttonClicks = new EventDispatcher<MouseEvent>();
constructor(buttons: Button[]) {
buttons.forEach(button => {
// 转发按钮点击事件
button.onClick.subscribe(event => {
this.buttonClicks.dispatch(event);
});
});
}
}
类型安全的事件设计
利用 TypeScript 泛型特性,可以构建类型安全的事件系统:
// 定义事件类型
type ApplicationEvents = {
"user.login": { username: string };
"data.loaded": { timestamp: number };
};
// 类型安全的事件调度器
class TypedEventDispatcher<T extends Record<string, unknown>> {
private dispatchers = new Map<keyof T, EventDispatcher<T[keyof T]>>();
on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
const dispatcher = this.getDispatcher(event);
return dispatcher.subscribe(handler);
}
dispatch<K extends keyof T>(event: K, data: T[K]) {
this.getDispatcher(event).dispatch(data);
}
private getDispatcher<K extends keyof T>(event: K) {
if (!this.dispatchers.has(event)) {
this.dispatchers.set(event, new EventDispatcher<T[K]>());
}
return this.dispatchers.get(event)!;
}
}
性能优化策略
- 事件节流:对于高频事件(如鼠标移动),可通过时间戳控制触发频率
class ThrottledEventDispatcher<T> extends EventDispatcher<T> {
private lastDispatch = 0;
private interval: number;
constructor(intervalMs: number) {
super();
this.interval = intervalMs;
}
public dispatch(value: T) {
const now = Date.now();
if (now - this.lastDispatch > this.interval) {
this.lastDispatch = now;
super.dispatch(value);
}
}
}
- 事件池化:对于大量相似事件,可复用事件对象减少内存分配
常见问题与解决方案
循环引用导致的内存泄漏
问题:事件订阅者持有调度器对象引用,导致无法被垃圾回收。
解决方案:使用弱引用或提供显式的取消订阅机制:
// 推荐模式:使用订阅对象取消订阅
const subscription = dispatcher.subscribe(handler);
// 不再需要时取消订阅
subscription.unsubscribe();
异步错误处理
问题:AsyncEventDispatcher 中单个订阅者抛出的错误会影响整个事件流。
解决方案:实现错误隔离机制:
public async dispatch(value: T): Promise<void> {
const handlers = this.notifySubscribers(value);
await Promise.all(handlers.map(p =>
p.catch(error => {
console.error("事件处理错误:", error);
})
));
}
总结与最佳实践
Motion Canvas 事件系统通过分层设计提供了灵活的事件处理能力,在实际开发中建议遵循以下最佳实践:
-
接口隔离:始终通过
subscribable属性暴露事件,而非直接暴露调度器// 推荐 public get onValueChanged() { return this.dispatcher.subscribable; } // 不推荐 public dispatcher = new EventDispatcher(); -
类型安全:为事件数据定义明确的 TypeScript 接口
-
异步边界:明确区分同步/异步事件场景,避免混用导致的逻辑混乱
-
资源管理:对于临时订阅,确保在组件销毁时取消订阅
完整的事件系统源码可参考 packages/core/src/events/ 目录,更多高级用法可研究官方示例中的 交互场景实现。
通过掌握 EventDispatcher 与 AsyncEventDispatcher 的使用技巧,开发者可以构建出响应式强、可维护性高的动画交互系统,为 Motion Canvas 项目增添丰富的交互体验。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



