EventEmitter — это паттерн проектирования, позволяющий объектам взаимодействовать через события. Объект, реализующий EventEmitter, может излучать события (emit), а другие объекты могут подписываться на эти события (on) и получать уведомления при их возникновении. Этот подход повышает модульность кода и упрощает обработку асинхронных действий.
В библиотеке Cannon.js EventEmitter используется для уведомления о событиях физического движка, таких как столкновения тел, изменения состояния, окончания симуляции. Однако собственную реализацию EventEmitter можно создать с нуля, чтобы глубже понять механизм работы и оптимизировать её под конкретные задачи.
Минимальная структура EventEmitter включает три базовых метода:
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
}
off(eventName, listener) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(l => l !== listener);
}
emit(eventName, ...args) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(listener => listener(...args));
}
}
Ключевые моменты:
this.events хранит массив слушателей для каждого
события.on создаёт массив, если его ещё нет, и добавляет
функцию-слушатель.off удаляет конкретную функцию, оставляя другие
подписки нетронутыми.emit вызывает все слушатели с переданными
аргументами.Иногда необходимо, чтобы событие вызывалось только один раз. Для
этого реализуется метод once:
once(eventName, listener) {
const wrapper = (...args) => {
listener(...args);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
}
Особенности реализации:
wrapper, которая вызывает
оригинальный слушатель.wrapper удаляется из списка
подписчиков, предотвращая повторное срабатывание.Для интеграции с объектами Cannon.js часто нужно передавать контекст
или дополнительные параметры при вызове слушателя. Это реализуется через
замыкания или метод bind:
class EventEmitterWithContext extends EventEmitter {
emit(eventName, ...args) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(listener => listener.apply(this, args));
}
}
apply(this, args) позволяет слушателям
использовать this как ссылку на объект-эмиттер.Для управления ресурсами и предотвращения утечек памяти полезно уметь очищать все подписки:
removeAllListeners(eventName) {
if (eventName) {
delete this.events[eventName];
} else {
this.events = {};
}
}
Иногда требуется, чтобы определённые обработчики вызывались раньше других. Для этого можно хранить слушателей в виде объектов с приоритетом:
on(eventName, listener, priority = 0) {
if (!this.events[eventName]) this.events[eventName] = [];
this.events[eventName].push({ listener, priority });
this.events[eventName].sort((a, b) => b.priority - a.priority);
}
emit(eventName, ...args) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(obj => obj.listener(...args));
}
В Cannon.js объекты Body, World,
Constraint могут использовать EventEmitter для уведомления
о физических событиях. С собственной реализацией:
class Body extends EventEmitter {
constructor(mass = 1) {
super();
this.mass = mass;
this.velocity = { x: 0, y: 0, z: 0 };
}
applyForce(force) {
this.velocity.x += force.x / this.mass;
this.velocity.y += force.y / this.mass;
this.velocity.z += force.z / this.mass;
this.emit('forceApplied', force);
}
}
const ball = new Body(2);
ball.on('forceApplied', (force) => {
console.log('Применена сила:', force);
});
ball.applyForce({ x: 10, y: 0, z: 0 });
emit('forceApplied', force) уведомляет всех слушателей
о воздействии силы.once,
чтобы снижать нагрузку на сборщик мусора.Эта реализация EventEmitter обеспечивает полное управление событиями, легко расширяется и полностью совместима с объектами Cannon.js, позволяя строить масштабируемые и гибкие системы физического взаимодействия.