如何实现Web-API EventTarget功能到一个类已经扩展了另一个类?



我经常遇到这样的问题:想从库中扩展一个类(一个我无法控制的类),但又想让这个类具有EventTarget/EventEmitter的功能。

class Router extends UniversalRouter { 
...
// Add functionality of EventTarget
}

我还想让这个类成为一个EventTarget,这样它就可以调度事件和监听事件。它是不是EventTarget的实例并不重要,重要的是它的功能可以直接在对象上调用。

我已经尝试合并原型,虽然这确实复制了原型函数,当试图添加事件侦听器时,我得到一个错误:

Uncaught TypeError: Illegal invocation

class Router extends UniversalRouter { 
willNavigate(location) {
const cancelled = this.dispatchEvent(new Event('navigate', { cancellable: true }));
if(cancelled === false) {
this.navigate(location);
}
}
}
Object.assign(Router.prototype, EventTarget.prototype);

我知道Mixin模式,但我不知道如何使用它来扩展现有的类:

const eventTargetMixin = (superclass) => class extends superclass {
// How to mixin EventTarget?
}

我不想要一个HAS-A关系,我在我的对象中创建一个新的EventTarget作为属性:

class Router extends UniversalRouter { 
constructor() {
this.events = new EventTarget();
} 
}
const eventTargetMixin = superclass =>
class extends superclass {
// How to mixin EventTarget?
}

…不是混合模式,它是纯继承(可能是名称&;动态子类&;&;动态子类型&;)。由于这一点和JavaScript实现的单继承,这就是所谓的并被广泛推广的"mixin"模式对于op描述的场景来说,毫无疑问是失败的。

因此,由于OP不希望依赖于聚合(不希望…this.events = new EventTarget();)必须提出一个真正的mixin,以确保任何OP的自定义路由器实例的真实Web-APIEventTarget行为。

但首先可以看看OP已经实现代理EventTarget行为的更改代码…

class UniversalRouter {
navigate(...args) {
console.log('navigate ...', { reference: this, args });
}
}
class ObservableRouter extends UniversalRouter {
// the proxy.
#eventTarget = new EventTarget;
constructor() {
// inheritance ... `UniversalRouter` super call.
super();
}
willNavigate(location) {
const canceled = this.dispatchEvent(
new Event('navigate', { cancelable: true })
);
if (canceled === false) {
this.navigate(location);
}
}
// the forwarding behavior.
removeEventListener(...args) {
return this.#eventTarget.removeEventListener(...args);
}
addEventListener(...args) {
return this.#eventTarget.addEventListener(...args);
}
dispatchEvent(...args) {
return this.#eventTarget.dispatchEvent(...args);
}
};
const router = new ObservableRouter;
router.addEventListener('navigate', evt => {
evt.preventDefault();
const { type, cancelable, target } = evt;
console.log({ type, cancelable, target });
});
router.willNavigate('somewhere');
.as-console-wrapper { min-height: 100%!important; top: 0; }

…它在私有字段#eventTarget上工作,其中后者是一个真正的EventTarget实例,通过转发原型方法访问,人们确实期望事件目标具有。

尽管上述实现按预期工作,但一旦开始遇到类似OP解释的场景,人们就会发现自己想要抽象基于代理的转发…

我经常遇到这样的问题:想从库中扩展一个类(一个我不控制的类),但也有一个类具有EventTarget/EventEmitter的功能。

我还想让这个类成为一个EventTarget,这样它就可以调度事件和监听事件。它是不是EventTarget的实例并不重要,重要的是它的功能可以直接在对象上调用。

由于函数(箭头函数除外)能够访问this上下文,因此可以将转发代理功能实现为我个人喜欢引用的基于函数的mixin.

此外,即使这样的实现可以用作构造函数,它也不是。也不鼓励这样使用。相反,它总是必须应用于任何对象,像这样withProxyfiedWebApiEventTarget.call(anyObject)……其中anyObject之后的功能是所有事件目标的方法,如dispatchEvent,addEventListenerremoveEventListener

// function-based `this`-context aware mixin
// which implements a forwarding proxy for a
// real Web-API EventTarget behavior/experience.
function withProxyfiedWebApiEventTarget() {
const observable = this;
// the proxy.
const eventTarget = new EventTarget;
// the forwarding behavior.
function removeEventListener(...args) {
return eventTarget.removeEventListener(...args);
}
function addEventListener(...args) {
return eventTarget.addEventListener(...args);
}
function dispatchEvent(...args) {
return eventTarget.dispatchEvent(...args);
}
// apply behavior to the mixin's observable `this`.
Object.defineProperties(observable, {
removeEventListener: {
value: removeEventListener,
},
addEventListener: {
value: addEventListener,
},
dispatchEvent: {
value: dispatchEvent,
},
});
// return observable target/type.
return observable
}
class UniversalRouter {
navigate(...args) {
console.log('navigate ...', { reference: this, args });
}
}
class ObservableRouter extends UniversalRouter {
constructor() {
// inheritance ... `UniversalRouter` super call.
super();
// mixin ... apply the function based
//           proxyfied `EventTarget` behavior.
withProxyfiedWebApiEventTarget.call(this);
}
willNavigate(location) {
const canceled = this.dispatchEvent(
new Event('navigate', { cancelable: true })
);
if (canceled === false) {
this.navigate(location);
}
}
};
const router = new ObservableRouter;
router.addEventListener('navigate', evt => {
evt.preventDefault();
const { type, cancelable, target } = evt;
console.log({ type, cancelable, target });
});
router.willNavigate('somewhere');
.as-console-wrapper { min-height: 100%!important; top: 0; }

这个答案与其他针对可观察或事件目标行为的问题密切相关。因此,OP要求的用例/场景之外的其他用例/场景将被链接到…

  1. 扩展Web-APIEventTarget

    • "在自定义类上非法调用addEventListener.call(ob,…)">

    • "实现与其他代码并行运行的函数调用队列">

  2. 为ES/JS对象类型实现自己/自定义的事件调度系统

    • "如何实现ES/JS对象类型的事件调度系统?">

编辑…将上述EventTarget特定的代理转送器/转发混合的方法/模式进一步推进,还有另一种实现,它从传递的类构造函数中通用地创建这样的混合…

const withProxyfiedWebApiEventTarget =
createProxyfiedForwarderMixinFromClass(
EventTarget, 'removeEventListener', 'addEventListener', 'dispatchEvent'
//EventTarget, ['removeEventListener', 'addEventListener', 'dispatchEvent']
);
class UniversalRouter {
navigate(...args) {
console.log('navigate ...', { reference: this, args });
}
}
class ObservableRouter extends UniversalRouter {
constructor() {
// inheritance ... `UniversalRouter` super call.
super();
// mixin ... apply the function based
//           proxyfied `EventTarget` behavior.
withProxyfiedWebApiEventTarget.call(this);
}
willNavigate(location) {
const canceled = this.dispatchEvent(
new Event('navigate', { cancelable: true })
);
if (canceled === false) {
this.navigate(location);
}
}
};
const router = new ObservableRouter;
router.addEventListener('navigate', evt => {
evt.preventDefault();
const { type, cancelable, target } = evt;
console.log({ type, cancelable, target });
});
router.willNavigate('somewhere');
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
function isFunction(value) {
return (
'function' === typeof value &&
'function' === typeof value.call &&
'function' === typeof value.apply
);
}
function isClass(value) {
let result = (
isFunction(value) &&
(/class(s+[^{]+)?s*{/).test(
Function.prototype.toString.call(value)
)
);
if (!result) {
// - e.g. as for `EventTarget` where
//   Function.prototype.toString.call(EventTarget)
//   returns ... 'function EventTarget() { [native code] }'.
try { value(); } catch({ message }) {
result = (/construct/).test(message);
}
}
return result;
}
function createProxyfiedForwarderMixinFromClass(
classConstructor, ...methodNames
) {
// guards.
if (!isClass(classConstructor)) {
throw new TypeError(
'The 1st arguments needs to be a class constructor.'
);
}
methodNames = methodNames
.flat()
.filter(value => ('string' === typeof value));
if (methodNames.length === 0) {
throw new ReferenceError(
'Not even a single to be forwarded method name got provided with the rest parameter.'
);
}
// mixin implementation which gets created/applied dynamically.
function withProxyfiedForwarderMixin(...args) {
const mixIntoThisType = this;
const forwarderTarget = new classConstructor(...args) ?? {};
const proxyDescriptor = methodNames
.reduce((descriptor, methodName) =>
Object.assign(descriptor, {
[ methodName ]: {
value: (...args) =>
forwarderTarget[methodName]?.(...args),
},
}), {}
);
Object.defineProperties(mixIntoThisType, proxyDescriptor);
return mixIntoThisType;
}
return withProxyfiedForwarderMixin;
}
</script>

最新更新