DOM 事件是前端開發者習覺得常的東西. 事件的監聽和觸發使用起來都很是方便, 可是他們的原理是什麼呢? 瀏覽器是怎樣處理 event綁定和觸發的呢?javascript
讓咱們經過實現一個簡單的event 處理函數, 來詳細瞭解一下.html
這個相比你們都很清楚了, 有三種註冊方式:前端
<button onclick="alert('hello!');">Say Hello!</button>
onXXX
屬性賦值document.getElementById('elementId').onclick = function() { console.log('I clicked it!') }
addEventListener()
註冊事件 (好處是能註冊多個 event handler)document.getElementById('elementId').addEventListener( 'click', function() { console.log('I clicked it!') }, false )
簡單的來講: event 的傳遞是 先自頂向下, 再自下而上java
完整的來講: event 的傳遞分爲兩個階段: capture 階段 和 bubble 階段git
讓咱們來看一個具體的例子:github
<html> <head> </head> <body> <div id="parentDiv"> <a id="childButton" href="https://github.com"> click me! </a> </div> </body> </html>
當咱們點擊上面這段 html 代碼中的 a 標籤時. 瀏覽器會首先計算出從 a 標籤到 html 標籤的節點路徑 (即: html => body => div => a
).api
而後進入 capture 階段: 依次觸發註冊在html => body => div => a
上的 capture 類型的 click event handler.數組
到達 a 節點後. 進入 bubble 階段. 依次出發 a => div => body => html
上註冊的 bubble 類型的 click event handler.瀏覽器
最後當 bubble 階段到達 html 節點後, 會出發瀏覽器的默認行爲(對於該例的 a 標籤來講, 就是跳轉到指定的網頁.)dom
從下圖咱們能夠更直觀的看到 event 的傳遞流程.
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) }
上面的代碼很是直觀, addEventListener 會根據 eventName 和 phase 將 handler 保存在 __handler
數組中, 其中 capture 類型的 handler 和 bubble 類型的 handler 分開保存.
爲了便於理解, 這裏咱們嘗試實現一個簡單版本的 event 出發函數 handler()
(這並非瀏覽器處理 event 的源碼, 但思路是相同的)
首先讓咱們理清瀏覽器處理 event 的流程步驟:
function initEvent(targetNode) { let ev = new Event() ev.target = targetNode // ev.target 是當前用戶真正出發的節點 ;(ev.isPropagationStopped = false), // 是否中止event的傳播 (ev.isDefaultPrevented = false) // 是否阻止瀏覽器默認的行爲 ev.stopPropagation = function() { this.isPropagationStopped = true } ev.preventDefault = function() { this.isDefaultPrevented = true } return ev }
function calculateNodePath(event) { let target = event.target let elements = [] // 用於存儲從當前節點到html節點的 節點路徑 do elements.push(target) while ((target = target.parentNode)) return elements.reverse() // 節點順序爲: targetElement ==> html }
// 依次觸發 capture類型的handlers, 順序爲: 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) } } }
function executeInPropertyHandler(ev) { if (!ev.isPropagationStopped) { ev.target['on' + ev.type].call(ev.target, ev) } }
// 基本上和 capture 階段處理方式相同 // 惟一的區別是 handlers 是逆向遍歷的: 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) } } }
function executeNodeDefaultHehavior(ev) { if (!isDefaultPrevented) { // 對於 a 標籤, 默認行爲就是跳轉連接 if (ev.type === 'click' && ev.tagName.toLowerCase() === 'a') { window.location = ev.target.href } // 對於其餘標籤, 瀏覽器會有其餘的默認行爲 } }
// 1.建立event對象, 初始化須要的數據 let event = initEvent(currentNode) function handleEvent(event) { // 2.計算觸發 event事件的DOM節點到html節點的**節點路徑 let elements = calculateNodePath(event) // 3.觸發capture類型的handlers executeCaptureHandlers(elements, event) // 4.觸發綁定在 onXXX 屬性上的 handler executeInPropertyHandler(event) // 5.觸發bubble類型的handlers executeBubbleHandlers(elements, event) // 6.觸發該DOM節點的瀏覽器默認行爲 executeNodeDefaultHehavior(event) }
以上就是當用戶出發 DOM event 時, 瀏覽器的大體處理流程.
咱們知道 event 有 stopPropagation()
和 preventDefault()
兩個方法, 他們的做用分別是:
stopPropagation()
stopPropagation()
後, 後續的 handler 將不會被觸發.preventDefault()
<a>
標籤不進行跳轉,<form>
標籤點擊 submit 後不自動提交表單.當咱們須要對 event handler 執行流進行精細操控時, 這兩個方法會很是有用.
addEventListener()
最後一個參數爲 false註冊 event handler 時, 瀏覽器默認是註冊的 bubble 類型 (即默認狀況下注冊的 event handler 觸發順序爲: 從當前節點到 html 節點)
addEventListener()
的實現是 native codeaddEventListener是由瀏覽器提供的 api, 並不是 JavaScript 原生 api. 用戶觸發 event 時, 瀏覽器會向 message queue
中加入 task, 並經過 Event Loop 執行 task 實現回調的效果.
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
這裏是個人博客的 github 地址, 歡迎 star & fork :tada:
郵箱: ssthouse@163.com