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:
- Registration in html tags
<button onclick="alert('hello!');">Say Hello!</button>
- Assignment of onXXX attribute to DOM node
document.getElementById('elementId').onclick = function() { console.log('I clicked it!') }
- 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:
- Create event objects to initialize the required data
- Calculate the DOM path from the DOM node that triggers the event event to the html node
- handlers that trigger the capture type
- Trigger handler bound to onXXX attribute
- handlers that trigger bubble s
- 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:
If you think this article is good, click on the link below to pay attention to it.
Want to contact me directly?
Mailbox: ssthouse@163.com