为typescript类自动生成和链接mixins



我有

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.log(`Event type: ${event.type}, payload:`, event.payload);
},
};

下面是我想做的mixins:

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.log(`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 }"

有什么问题?

当我这样做的时候,我有两个问题。

  1. 最大的问题:每次我创建一个新的事件类型,我需要实现的混合以及,这是99%相同的所有其他混合。我想让它自动化。理想情况下,通过调用createEventMixin<CustomEvent1>()来创建emitCustomEvent1方法。这可能吗?
  2. 当我向类添加越来越多的事件时,写像producesCustomEvent1(producesCustomEvent2(BaseClass))这样的东西并不真正可读。这就是不使用generic的原因,因为在某些情况下会产生很多不同的事件。有没有一种方法可以拥有类似类型构建器的东西?比如const CustomEventEmitter = Builder(BaseClass).withProducingEvent1().withProducingEvent2().return()

OP试图通过…达到的目的

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方法,如emitCustomEvent1emitCustomEvent2,它们都是为了通过一个和相同的自定义发射器发送不同(类型)的事件,EventTargetdispatchEvent方法覆盖了这些方法,其中事件类型由特定类型的字符串或通过事件类对象的type属性直接寻址。后者的类型安全(没有事件欺骗的机会)是通过封装(并随后读取)每个新添加的事件侦听器创建的初始事件来实现的。

浏览器和Node.js已经支持EventTarget了,因此有了这样一个"信号和插槽">系统。但是为了实现OP的附加任务,即…

  • 定义/注册特定事件类型的基础数据,
  • 跟踪任何已分派事件的类型、有效载荷、目标和消费者,

…一个人需要自己实现这样一个系统。在此基础上,可以实现OP正在寻找的功能。

上述基于EventTargetMixin实现的函数的链接JavaScript变体需要更改为具有两个以上的方法…putBaseEventDatadeleteBaseEventData…除了已经存在的…dispatchEvent,hasEventListener,addEventListener,removeEventListener.

putBaseEventData方法是OP试图通过为预定义的(和不同命名的)自定义事件创建不同命名的自定义调度方法来实现的附属项。

并且可追溯性特性可以通过一些简单的事情来实现,例如使mixin在mixin应用时意识到接受tracer方法。这个跟踪程序在内部被传递给任何新添加的事件处理程序(addEventListener方法必须相应地进行调整),以便额外启用处理程序的handleEvent方法,将所有感兴趣的数据传递给tracer

注意

如果下面提供的可跟踪的"putable">事件目标方法可以解决OP的问题,则需要将JavaScript mixin重写为TypeScript类,以便从它派生出自己的自定义类型/类。

// - tracer function ...
//   ...could be later renamed to `collectAllDispatchedEventData`
function traceAnyDispatchedEventData({ type, baseData, event, consumer }) {
console.log({ 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 (/^[objects+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>

注意:

最新更新