Front-end chatter: DOM event principle

Keywords: Front-end Attribute github Javascript

DOM events are common to front-end developers. Event monitoring and triggering are very convenient to use, but what are their principles? How do browsers handle event binding and triggering?

Let's look at it in detail by implementing a simple event handler.

First, how to register event?

As we all know, there are three ways to register:

  1. Registration in html tags
<button onclick="alert('hello!');">Say Hello!</button>
  1. Assignment of onXXX attribute to DOM node
document.getElementById('elementId').onclick = function() {
  console.log('I clicked it!')
}
  1. Register events using addEventListener() (the advantage is that multiple event handlers can be registered)
document.getElementById('elementId').addEventListener(
  'click',
  function() {
    console.log('I clicked it!')
  },
  false
)

How does event pass between DOM nodes?

Simply put: event is passed from top to bottom, then from bottom to top.

Completely speaking: event transmission can be divided into two stages: capture stage and bubble stage.

Let's take a concrete example:

<html>
  <head> </head>

  <body>
    <div id="parentDiv">
      <a id="childButton" href="https://github.com"> click me! </a>
    </div>
  </body>
</html>

When we click the label a in the html code above, the browser first calculates the node path from the label a to the html label (that is, html => body => div => a).

Then enter the capture stage: trigger click event handler of capture type registered on HTML => body => div => a in turn.

After arriving at a node, it enters the bubbles stage. It starts a => div => body => click event handler of bubbles type registered on HTML in turn.

Finally, when the bubble stage arrives at the html node, the default behavior of the browser (for the label a in this case, jump to the specified page.)

From the following figure, we can see more intuitively the delivery process of event.

So how does such event transfer flow work?

Let's look at the code implementation of addEventListener:

HTMLNode.prototype.addEventListener = function(eventName, handler, phase) {
  if (!this.__handlers) this.handlers = {}
  if (!this.__handlers[eventName]) {
    this.__handlers[eventName] = {
      capture: [],
      bubble: []
    }
  }
  this.__handlers[eventName][phase ? 'capture' : 'bubble'].push(handler)
}

The code above is very intuitive, and addEventListener saves handlers in the _handler array based on event Name and phase, where the handlers of capture type and bubble type are saved separately.

Next comes the core of this article: How does event trigger handler?

For ease of understanding, here we try to implement a simple version of event starting function handler() (this is not the browser's source code for event processing, but the idea is the same)

First, let's clarify the process steps of browser processing event:

  1. Create event objects to initialize the required data
  2. Calculate the DOM path from the DOM node that triggers the event event to the html node
  3. handlers that trigger the capture type
  4. Trigger handler bound to onXXX attribute
  5. handlers that trigger bubble s
  6. Browser default behavior triggering the DOM node
1. Create event objects to initialize the required data
function initEvent(targetNode) {
  let ev = new Event()
  ev.target = targetNode // ev.target is the real starting point for current users
  ;(ev.isPropagationStopped = false), // Does event Stop Spreading
    (ev.isDefaultPrevented = false) // Whether to block the default behavior of browsers

  ev.stopPropagation = function() {
    this.isPropagationStopped = true
  }
  ev.preventDefault = function() {
    this.isDefaultPrevented = true
  }
  return ev
}
2. Calculate the node path from DOM node triggering event event to html node
function calculateNodePath(event) {
  let target = event.target
  let elements = [] // Node paths used to store nodes from the current node to the html node
  do elements.push(target)
  while ((target = target.parentNode))
  return elements.reverse() // The order of nodes is: targetElement ==> HTML
}
3. Triggering handlers of type capture
// handlers of capture type are triggered in turn, in the order HTML ==> targetElement
function executeCaptureHandlers(elements, ev) {
  for (var i = 0; i < elements.length; i++) {
    if (ev.isPropagationStopped) break

    var curElement = elements[i]
    var handlers =
      (currentElement.__handlers &&
        currentElement.__handlers[ev.type] &&
        currentElement.__handlers[ev.type]['capture']) ||
      []
    ev.currentTarget = curElement
    for (var h = 0; h < handlers.length; h++) {
      handlers[h].call(currentElement, ev)
    }
  }
}
4. Trigger handler bound to onXXX attribute
function executeInPropertyHandler(ev) {
  if (!ev.isPropagationStopped) {
    ev.target['on' + ev.type].call(ev.target, ev)
  }
}
5. Triggering handlers of bubble s type
// Basically the same way as in the capture phase
// The only difference is that handlers are traversed backwards: targetElement ==> HTML

function executeBubbleHandlers(elements, ev) {
  elements.reverse()
  for (let i = 0; i < elements.length; i++) {
    if (isPropagationStopped) {
      break
    }
    var handlers =
      (currentElement.__handlers &&
        currentElement.__handlers[ev.type] &&
        currentElement.__handelrs[ev.type]['bubble']) ||
      []
    ev.currentTarget = currentElement
    for (var h = 0; h < handlers.length; h++) {
      handlers[h].call(currentElement, ev)
    }
  }
}
6. Trigger the default behavior of the browser that triggers the DOM node
function executeNodeDefaultHehavior(ev) {
  if (!isDefaultPrevented) {
    // For label a, the default behavior is to jump links
    if (ev.type === 'click' && ev.tagName.toLowerCase() === 'a') {
      window.location = ev.target.href
    }
    // For other tags, browsers will have other default behavior
  }
}
Let's look at the complete invocation logic:
// 1. Create event objects to initialize the required data
let event = initEvent(currentNode)

function handleEvent(event) {
  // 2. Calculate the ** node path from DOM node to html node triggering event event
  let elements = calculateNodePath(event)
  // 3. Triggering handlers of type capture
  executeCaptureHandlers(elements, event)
  // 4. Trigger handler bound to onXXX attribute
  executeInPropertyHandler(event)
  // 5. Triggering handlers of bubble s type
  executeBubbleHandlers(elements, event)
  // 6. Trigger the default behavior of the browser that triggers the DOM node
  executeNodeDefaultHehavior(event)
}

This is the browser's general process flow when the user starts DOM event.

propagation && defaultBehavior

We know that event has stopPropagation() and preventDefault() methods. Their functions are:

stopPropagation()
  • Stop propagation of event. As you can see from the code above, after calling stopPropagation(), the subsequent handler will not be triggered.
preventDefault()
  • The default behavior of the browser is not triggered. For example, the < a > tag does not jump and the < form > tag does not submit the form automatically after clicking submit.

These two methods can be very useful when we need to fine-tune the flow of event handler execution.

Some supplements~

The last parameter of the default addEventListener() is false

When registering event handler, the browser defaults to the registered bubble type (that is, the order of event handler triggers registered by default is: from the current node to the html node)

The implementation of addEventListener() is native code

AddiEventListener is an API provided by browser, not native JavaScript api. When a user triggers event, the browser adds task to message queue and executes task through Event Loop to achieve callback effect.

reference links:

https://www.bitovi.com/blog/a-crash-course-in-how-dom-events-work

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events

Want to know more about front-end / D3.js / data visualization?

Here is the github address of my blog. Welcome Star & fork: tada:

D3-blog

If you think this article is good, click on the link below to pay attention to it.

github home page

Know about columns

Nuggets

Want to contact me directly?

Mailbox: ssthouse@163.com

Posted by jocknerd on Wed, 15 May 2019 08:35:54 -0700