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

问题描述 投票:0回答:1

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

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

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

我尝试合并原型,虽然这确实复制了原型函数,但在尝试添加事件侦听器时,我收到错误:

未捕获的类型错误:非法调用

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();
  } 
}
javascript inheritance proxy mixins es6-class
1个回答
1
投票
const eventTargetMixin = superclass =>
  class extends superclass {
    // How to mixin EventTarget?
  }

...不是混合模式,它是纯继承(可能是名称“动态子类”“动态子类型”。由于这一点和 JavaScript 只实现了单一继承,这种所谓的并广泛推广的“mixin”模式毫不奇怪地在OP描述的场景中失败了。

因此,由于 OP 不想依赖聚合(不想......

this.events = new EventTarget();
),必须想出一个真正的 mixin,以确保任何 OP 自定义的真正的 Web-API
EventTarget
行为路由器实例。

但是第一个可能会查看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; }

...它适用于 private 字段

#eventTarget
,其中后者是一个真正的
EventTarget
实例,可以通过人们期望事件目标具有的转发原型方法进行访问。

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

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

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

由于函数(箭头函数除外)能够访问

this
上下文,因此可以将转发代理功能实现为我个人喜欢称为 基于函数的混合

此外,尽管这样的 实现可以用作构造函数,但它不是 并且也不鼓励这样使用。相反,它总是必须应用于像这样的任何对象...

withProxyfiedWebApiEventTarget.call(anyObject)
...其中
anyObject
随后具有事件目标的所有方法,例如
dispatchEvent
addEventListener
removeEventListener

// 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-API

    EventTarget

  2. 为 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>

© www.soinside.com 2019 - 2024. All rights reserved.