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:
這裏是個人博客的 github 地址, 歡迎 star & fork :tada:
郵箱: ssthouse@163.com