The introduction

This is a simple need, derived thinking.

Yesterday QA introduced a requirement for rich text optimization. When inserting video and audio, if the user clicks on both video and audio at the same time, the other video or audio should stop playing, and only the last one clicked will play.

The requirements are simple (naive thinking), a little clarity:

  • After the multimedia file is inserted, get all the multimedia elements in the DOM
  • The loop adds events to them, removing previous events before adding them
  • When any element is clicked, iterate through the list of multimedia files, how about suspending everything else instead of yourself

Three steps.

In the code

 onEmbed() {
      const item = document.querySelectorAll('video,audio')
      item.forEach((element, kindex) = > {
        const clickFun = e= > {
          item.forEach((i, index) = > {
            // If the content contains audio and video, the audio and video can be played simultaneously
            if(index ! == kindex) { i.pause() } }) } element.removeEventListener('play', clickFun, false)
        element.addEventListener('play', clickFun, false)})},Copy the code

The code looks perfect, but when I run it, it turns out there’s no problem. When multiple videos are clicked, only the last one will always play and the others will only pause.

If it were that simple, there wouldn’t be a blog entry. This code has serious performance issues. If you’re interested, stop and think for a minute

console.log("It's impossible to think, I just want to know right away!")
Copy the code

Ok, now that I’ve revealed that if I print console.log(e) in the clickFun event, I’ll see that every time I add a video, E will print multiple times, similar to the image below. This will cause this event to be called multiple times as more elements are added, so I need to work on that. You also need to figure out why removeEventListener doesn’t work.

RemoveEventListener introduction

If you read the code carefully, it should be clickFun. Each time it is generated, a new event is generated, while removeEventListener removes the event added by addEventListener. Function must be a reference object

Developer.mozilla.org/zh-CN/docs/…

The addEventListener method registers the specified listener with EventTarget, and the removeEventListener is followed by, The type, listener, and useCapture parameters are used to delete events from the EventTarget. If there is one difference, the Event will fail.

Developer.mozilla.org/zh-CN/docs/…

Now it’s a listener because it’s triggered every time, so there’s always a new one in memory, so you can’t remove it

Fill in the pit

Method 1: Find the element, iterate through all the events, and remove them. For example, the Event Listeners tool in Chrome can monitor the element’s events and remove them

Unfortunately, this method is currently only available in Chrome Console and is not yet standard.

In 2015, the W3C blog announced the creation of the WICG (Web Platform Incubator Community Group), calling on developers to submit viable new Web Platform features to this Group. There are getEventListeners, It has been mentioned for two years, but there seems to be no progress.

On Github, there was also an issue to WHATWG (a great organization, not worse than W3C), with more detailed suggestions and a lot of people talking about it. I think WHATWG will make it a new feature sooner.

  • GetEventListeners of Chrome
  • GetEventListeners on the WICG Panel’s recommendations
  • GetEventListeners are on Github organized by WHATWG

Remove clickFun from the loop as long as the listener, type, and useCapture parameters are the same.

Look at the code

// clickFun is placed in the outermost layer to ensure that it is generated only once
const clickFun = e= > {
  const item = document.querySelectorAll('video,audio')
  item.forEach((i, index) = > {
    // If the content contains audio and video, the audio and video can be played simultaneously
    if(index ! = =Number(e.target.getAttribute('kindex'))) {
      i.pause()
    }
  })
}

 onEmbed() {
      const item = document.querySelectorAll('video,audio')
      item.forEach((element, kindex) = > {
        element.setAttribute('kindex', kindex)
        element.removeEventListener('play', clickFun, false)
        element.addEventListener('play', clickFun, false)})}Copy the code

The code runs, perfect, and everything works as it was originally intended.

Is that the end of the matter? If we need to remove all events of a certain type from a DOM node, what should we do? What should I do if I need to view all events on a node? And the current native add, remove events are really difficult to use, is there a simpler way to write.

These are the new requirements, and you can find the latter through The Chrome Event Listeners tool, but not through coding.

In fact, if we think carefully, the first problem is also easy to solve. If we can’t get the Event of the node, we should store the Event in a list before adding the Event each time. If we want to remove, we just need to traverse the list, so that all the events can be managed, and it is convenient to operate in the future.

Okay, so now that we have the idea, here’s the stroking code.

type Capture = boolean | AddEventListenerOptions;
interface EventHandler {
  [handlers: string]: [[EventListenerOrEventListenerObject, Capture]] | [];
}

const isProperty = (obj, property) = >
  Object.prototype.hasOwnProperty.call(obj, property);
const dom = {
  eventMap: new Map<Element, EventHandler>(),
  addListener(
    node: Element,
    event: string,
    handler: EventListenerOrEventListenerObject,
    capture: Capture
  ) {
    if (!this.eventMap.has(node)) {
      this.eventMap.set(node, Object.create(null));
    }

    // @ts-ignore
    const targetHandlers: EventHandler = this.eventMap.get(node);

    if(! isProperty(targetHandlers, event)) { targetHandlers[event] = []; }// @ts-ignore
    targetHandlers[event].push([handler, capture]);

    node.addEventListener(event, handler, capture);
  },
  removeListeners(node, event) {
    if (!this.eventMap.has(node)) {
      return false;
    }
    // @ts-ignore
    const targetHandlers: EventHandler = this.eventMap.get(node);
    if(! isProperty(targetHandlers, event)) {return false;
    }

    targetHandlers[event].forEach(th= >
      node.removeEventListener(event, th[0], th[1])); }};export default dom;


Copy the code

The code is very simple, less than 50 lines, but basically it’s adding, deleting, and listing existing listening events.

Deleting events also eliminates the need to pass a handler, which is much more convenient than before.

dom.addListener(evn, "click", clickfun, false);
dom.removeListeners(evn, "click");
Copy the code

The exported API makes it easy to see how many types of events exist under a node

Finally, I organized the code, made a demo, put it in the Codesandebox, all the code in it

Click on the demo