Вот что у меня есть
interface BaseEvent {
type: string;
payload: any;
}
interface EventEmitter {
emit(event: BaseEvent): void;
}
class BaseClass {
constructor(protected eventEmitter: EventEmitter) {}
emit(event: BaseEvent) {
this.eventEmitter.emit(event);
}
}
interface CustomEvent1 extends BaseEvent {
type: 'custom1';
payload: {
message: string;
};
}
interface CustomEvent2 extends BaseEvent {
type: 'custom2';
payload: {
value: number;
};
}
const eventEmitter: EventEmitter = {
emit(event: BaseEvent) {
console.info(`Event type: ${event.type}, payload:`, event.payload);
},
};
Вот что я хочу сделать с миксинами:
type Constructor<T> = new (...args: any[]) => T;
function producesCustomEvent1<TBase extends Constructor<BaseClass>>(Base: TBase) {
return class extends Base {
emitCustomEvent1(event: CustomEvent1) {
this.emit(event);
}
};
}
function producesCustomEvent2<TBase extends Constructor<BaseClass>>(Base: TBase) {
return class extends Base {
emitCustomEvent2(event: CustomEvent2) {
this.emit(event);
}
};
}
class CustomEventEmitter extends producesCustomEvent1(producesCustomEvent2(BaseClass)) {
constructor(eventEmitter: EventEmitter) {
super(eventEmitter);
}
}
const eventEmitter: EventEmitter = {
emit(event: BaseEvent) {
console.info(`Event type: ${event.type}, payload:`, event.payload);
},
};
const customEventEmitter = new CustomEventEmitter(eventEmitter);
const event1: CustomEvent1 = { type: 'custom1', payload: { message: 'Hello, world!' } };
customEventEmitter.emitCustomEvent1(event1); // Output: "Event type: custom1, payload: { message: 'Hello, world!' }"
const event2: CustomEvent2 = { type: 'custom2', payload: { value: 42 } };
customEventEmitter.emitCustomEvent2(event2); // Output: "Event type: custom2, payload: { value: 42 }"
В чем проблема?
Когда я делаю это так, у меня возникают две проблемы.
createEventMixin<CustomEvent1>()
, которое создает метод emitCustomEvent1
. Это возможно?producesCustomEvent1(producesCustomEvent2(BaseClass))
не очень читаемо, когда я добавляю в класс все больше и больше событий. И это причина, по которой вообще не следует использовать generic, потому что будут случаи, когда будет много разных созданных событий. Есть ли способ иметь что-то вроде построителя типов? Что-то вроде const CustomEventEmitter = Builder(BaseClass).withProducingEvent1().withProducingEvent2().return()
.По моему мнению, EventEmitter
всегда должен быть реализован в общем виде (либо как класс, либо как миксин), что означает, что всегда и везде есть только один метод emit
/dispatch
. Какой вид/тип события будет генерироваться, должно зависеть от контекста объекта, генерирующего событие. Также Event
, на мой взгляд, должен быть реализован в целом. В контексте испускания нужно решить, какие значения присваиваются свойствам event
и type
en payload
. Может быть, ОП прямо сейчас возится с проблемой, которой даже не существует.
@PeterSeliger @Dimava EventEmitter должен оставаться общим. Это также может быть что-то вроде .emit('custom1', payload)
. Дело в том, что классы, производящие события, должны через код указывать, какие события они производят, и иметь только функции для создания этих событий. Этим я собираюсь устранить слабость архитектуры, управляемой событиями, которая теряет обзор. Когда я могу выполнить свое требование, я могу автоматически создать диаграмму, какой объект производит/потребляет какие события.
Почему бы тогда не реализовать эмиттер событий таким образом, чтобы ... .emit(event)
считывал type
значение из переданного event
объекта, чтобы уведомить всех type
конкретных слушателей. Нет необходимости в пользовательских эмиттерах событий. Какой бы (наблюдаемый) объект (экземпляры классов, генерирующих события, упомянутых в OP) не вызывал emit
, просто нужно передать событие своего типа.
@PeterSeliger, потому что это делает практически невозможным создание диаграммы со всеми зависимостями, когда я не знаю, кто создает какое событие. Кроме того, я хочу реализовать то же самое для потребления событий, просто упустив это, чтобы упростить проблему.
@Yggdrasil ... 1/3 ... Любая emit
функциональность, которая является просто псевдонимом для EventTargetdispatchEvent
, требует наличия addEventListener
реализации. Последнее — это то самое место, где начинается управление всеми listeners
, следовательно, всеми типами событий и всеми обработчиками, связанными с каждым типом. Следовательно, через любой обработчик уже можно отслеживать любое отправленное событие и, следовательно, его тип и цель отправки.
@Yggdrasil ... 2/3 ... А что касается потребителей событий ... Можно было бы реализовать собственный тип TraceableEventTarget
, где можно было бы разрешить addEventListener
принимать необязательную функцию tracer
(обратный вызов) и / или необязательный объект defaultEventData
, где либо из них будет нести информацию о потребителе, который добавил слушателя.
@Yggdrasil ... 3/3 ... Я до сих пор не вижу необходимости в миксинах, создающих фабрики, когда все можно решить с помощью одной реализации, например. TraceableEventTarget
предоставляется либо как класс , либо как миксин.
Чего ОП пытается достичь с помощью ...
const customEventEmitter = new CustomEventEmitter(eventEmitter);
const event1: CustomEvent1 = { type: 'custom1', payload: { message: 'Hello, world!' } };
customEventEmitter.emitCustomEvent1(event1); // Output: "Event type: custom1, payload: { message: 'Hello, world!' }"
const event2: CustomEvent2 = { type: 'custom2', payload: { value: 42 } };
customEventEmitter.emitCustomEvent2(event2); // Output: "Event type: custom2, payload: { value: 42 }"
... где приведенный выше код вводит методы emit
с разными именами, такие как emitCustomEvent1
и emitCustomEvent2
, которые служат для отправки каждого события (типа) через один и тот же пользовательский эмиттер, покрывается методом EventTarget
dispatchEvent
, где к типу события напрямую обращаются либо через строку, специфичную для типа, либо через атрибут type
объекта, подобного событию. Безопасность типов (отсутствие возможности подмены событий) последнего достигается путем инкапсуляции (и последующего чтения) начального события, которое создается с каждым вновь добавленным прослушивателем событий.
Браузеры и Node.js уже поддерживают EventTarget
и поэтому имеют такую систему «Сигналы и слоты». Но для достижения дополнительных задач ОП, которые ...
... внедрять такую систему нужно самому. Вдобавок к этому можно реализовать функции, которые ищет OP.
Вышеприведенный связанный вариант JavaScript реализации функции на основе EventTargetMixin
необходимо изменить, добавив еще два метода... putBaseEventData
и deleteBaseEventData
... в дополнение к уже существующим... dispatchEvent
, hasEventListener
, addEventListener
и removeEventListener
.
Метод putBaseEventData
является подвеской к тому, чего пытается достичь OP, создавая настраиваемые методы отправки с разными именами для предопределенных (и именованных по-разному) настраиваемых событий и с ними.
И функция отслеживания достигается с помощью чего-то такого простого, как информирование миксина о принятии метода tracer
во время применения миксина. Этот трассировщик внутренне передается любому вновь добавленному обработчику событий (метод addEventListener
должен быть соответствующим образом адаптирован), чтобы дополнительно включить метод handleEvent
обработчика для передачи всех интересующих данных в tracer
.
Примечание
В случае, если следующий предоставленный отслеживаемый и «помещаемый» подход к целевому событию решит проблему OP, необходимо переписать миксин JavaScript в класс TypeScript, чтобы сделать из него собственные пользовательские типы/классы.
// - tracer function ...
// ...could be later renamed to `collectAllDispatchedEventData`
function traceAnyDispatchedEventData({ type, baseData, event, consumer }) {
console.info({ type, baseData, event, consumer });
}
class ObservableTraceableType {
constructor(name) {
this.name = name;
TraceablePutableEventTargetMixin.call(this, traceAnyDispatchedEventData);
}
}
// // for TypeScript ...
// class ObservableTraceableType extends TraceablePutableEventTarget { /* ... */ }
// // ... with a class based `TraceablePutableEventTarget` implementation.
const a = new ObservableTraceableType('A');
const b = new ObservableTraceableType('B');
const c = new ObservableTraceableType('C');
function cosumingHandlerX(/*evt*/) { /* do something with `evt` */}
function cosumingHandlerY(/*evt*/) { /* do something with `evt` */}
a.putBaseEventData('payload-with-value', { payload: { value: 1234 } });
a.putBaseEventData('payload-with-message', { payload: { message: 'missing' } });
a.addEventListener('payload-with-value', cosumingHandlerX);
a.addEventListener('payload-with-message', cosumingHandlerX);
a.addEventListener('payload-with-value', cosumingHandlerY);
a.dispatchEvent('payload-with-value');
a.dispatchEvent({
type: 'payload-with-message',
payload: { message: 'legally altered payload default message' },
});
function cosumingHandlerQ(/*evt*/) { /* do something with `evt` */}
function cosumingHandlerR(/*evt*/) { /* do something with `evt` */}
b.putBaseEventData('payload-with-value', { payload: { value: 5678 } });
b.putBaseEventData('payload-with-message', { payload: { message: 'default message' } });
b.addEventListener('payload-with-message', cosumingHandlerQ);
b.addEventListener('payload-with-value', cosumingHandlerQ);
b.addEventListener('payload-with-value', cosumingHandlerR);
b.dispatchEvent('payload-with-message');
b.dispatchEvent({
type: 'payload-with-value',
id: 'spoof-attempt-for_event-id',
target: { spoof: 'attempt-for_event-target' },
payload: { value: 9876, message: 'legally altered payload default message' },
});
function cosumingHandlerK(/*evt*/) { /* do something with `evt` */}
function cosumingHandlerL(/*evt*/) { /* do something with `evt` */}
c.addEventListener('non-prestored-event-data-FF', cosumingHandlerK);
c.addEventListener('non-prestored-event-data-FF', cosumingHandlerL);
c.addEventListener('non-prestored-event-data-GG', cosumingHandlerL);
c.dispatchEvent('non-prestored-event-data-FF');
c.dispatchEvent({
type: 'non-prestored-event-data-GG',
foo: 'FOO',
bar: { baz: 'BAZ' },
});
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// import `TraceablePutableEventTargetMixin` from module.
const TraceablePutableEventTargetMixin = (function () {
// implementation / module scope.
function isString(value/*:{any}*/)/*:{boolean}*/ {
return (/^\[object\s+String\]$/)
.test(Object.prototype.toString.call(value));
}
function isFunction(value/*:{any}*/)/*:{boolean}*/ {
return (
('function' === typeof value) &&
('function' === typeof value.call) &&
('function' === typeof value.apply)
);
}
// either `uuid` as of e.g. Robert Kieffer's
// ... [https://github.com/broofa/node-uuid]
// or ... Jed Schmidt's [https://gist.github.com/jed/982883]
function uuid(value)/*:{string}*/ {
return value
? (value^Math.random() * 16 >> value / 4).toString(16)
: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid);
}
function getSanitizedObject(value/*:{any}*/)/*:{Object}*/ {
return ('object' === typeof value) && value || {};
}
class Event {
constructor({
id/*:{string}*/ = uuid(),
type/*:{string}*/,
target/*:{Object}*/,
...additionalData/*:{Object}*/
}) {
Object.assign(this, {
id,
type,
target,
...additionalData
});
}
}
class TracingEventListener {
constructor(
target/*:{Object}*/,
type/*:{string}*/,
handler/*:{Function}*/,
tracer/*:{Function}*/,
) {
const initialEvent/*:{Event}*/ = new Event({ target, type });
function handleEvent(evt/*:{Object|string}*/, baseData/*:{Object}*/)/*:{void}*/ {
const {
id/*:{string|undefined}*/,
type/*:{string|undefined}*/,
target/*:{string|undefined}*/,
...allowedOverwriteData/*:{Object}*/
} = getSanitizedObject(evt);
// prevent spoofing of any trusted `initialEvent` data.
const trustedEvent/*:{Event}*/ = new Event(
Object.assign(
{}, getSanitizedObject(baseData), initialEvent, allowedOverwriteData
)
);
// handle event non blocking
setTimeout(handler, 0, trustedEvent);
// trace event non blocking
setTimeout(tracer, 0, {
type: trustedEvent.type, baseData, event: trustedEvent, consumer: handler,
});
};
function getHandler()/*:{Function}*/ {
return handler;
};
function getType()/*:{string}*/ {
return type;
};
Object.assign(this, {
handleEvent,
getHandler,
getType,
});
}
}
function TraceablePutableEventTargetMixin(tracer/*{Function}*/) {
if (!isFunction(tracer)) {
tracer = _=>_;
}
const observableTarget/*:{Object}*/ = this;
const listenersRegistry/*:{Map}*/ = new Map;
const eventDataRegistry/*:{Map}*/ = new Map;
function putBaseEventData(
type/*:{string}*/, { type: ignoredType/*:{any}*/, ...baseData/*:{Object}*/ }
)/*:{void}*/ {
if (isString(type)) {
eventDataRegistry.set(type, baseData);
}
}
function deleteBaseEventData(type/*:{string}*/)/*:{boolean}*/ {
let result = false;
if (isString(type)) {
result = eventDataRegistry.delete(type);
}
return result;
}
function addEventListener(
type/*:{string}*/, handler/*:{Function}*/,
)/*:{TracingEventListener|undefined}*/ {
let reference/*:{TracingEventListener|undefined}*/;
if (isString(type) && isFunction(handler)) {
const listeners/*:{Array}*/ = listenersRegistry.get(type) ?? [];
reference = listeners
.find(listener => listener.getHandler() === handler);
if (!reference) {
reference = new TracingEventListener(
observableTarget, type, handler, tracer
);
if (listeners.push(reference) === 1) {
listenersRegistry.set(type, listeners);
}
}
}
return reference;
}
function removeEventListener(
type/*:{string}*/, handler/*:{Function}*/,
)/*:{boolean}*/ {
let successfully = false;
const listeners/*:{Array}*/ = listenersRegistry.get(type) ?? [];
const idx/*:{number}*/ = listeners
.findIndex(listener => listener.getHandler() === handler);
if (idx >= 0) {
listeners.splice(idx, 1);
successfully = true;
}
return successfully;
}
function dispatchEvent(evt/*:{Object|string}*/ = {})/*:{boolean}*/ {
const type = (
(evt && ('object' === typeof evt) && isString(evt.type) && evt.type) ||
(isString(evt) ? evt : null)
);
const listeners/*:{Array}*/ = listenersRegistry.get(type) ?? [];
const baseData/*:{Object}*/ = eventDataRegistry.get(type) ?? {};
listeners
.forEach(({ handleEvent }) => handleEvent(evt, baseData));
// success state
return (listeners.length >= 1);
}
function hasEventListener(type/*:{string}*/, handler/*:{Function}*/)/*:{boolean}*/ {
return !!(
listenersRegistry.get(type) ?? []
)
.find(listener => listener.getHandler() === handler);
}
Object.defineProperties(observableTarget, {
putBaseEventData: {
value: putBaseEventData,
},
deleteBaseEventData: {
value: deleteBaseEventData,
},
addEventListener: {
value: addEventListener,
},
removeEventListener: {
value: function (
typeOrListener/*:{TracingEventListener|string}*/,
handler/*:{Function}*/,
)/*:{boolean}*/ {
return (
isString(typeOrListener) &&
isFunction(handler) &&
removeEventListener(typeOrListener, handler)
) || (
(typeOrListener instanceof TracingEventListener) &&
removeEventListener(typeOrListener.getType(), typeOrListener.getHandler())
) || false;
},
},
hasEventListener: {
value: function (
typeOrListener/*:{TracingEventListener|string}*/,
handler/*:{Function}*/,
)/*:{boolean}*/ {
return (
isString(typeOrListener) &&
isFunction(handler) &&
hasEventListener(typeOrListener, handler)
) || (
(typeOrListener instanceof TracingEventListener) &&
hasEventListener(typeOrListener.getType(), typeOrListener.getHandler())
) || false;
},
},
dispatchEvent: {
value: dispatchEvent,
},
});
// return observable target/type.
return observableTarget;
}
// module's default export.
return TraceablePutableEventTargetMixin;
}());
</script>
Примечание:
1. Почему бы и нет
extends EventEmitter
? 2. Почему бы и нет.emit('custom1', payload)
? 3. Нужна ли вам проверка во время выполнения или только проверка типов?