身爲一個前端碼農,在開發中遇到凡是須要與用戶互動或是須要由用戶觸發的功能,老是離不開事件處理。javascript
今天聊聊瀏覽器的 DOM 事件傳遞機制。html
在瀏覽器的 Javascript 引擎解析 HTML、SVG 時,會將內容分析成一個個的 DOM (Document Object Model),當用戶與 DOM 產生互動時,則是經過 DOM 上註冊的事件監聽器,去觸發某個事件。前端
例如常見的 onClick
、onTouchStart
,輸入框的 onInput
、onChange
、onBlur
等,都是經常使用的事件類型。java
例如咱們曾經最熟悉的 jQuery,咱們會用這樣的方式去註冊事件監聽:程序員
$('#id').on('click', function(){ ... })
但 jQuery 已經成爲明日黃花;在現代框架中,Vue 對註冊事件監聽器提供了一些語法糖,讓你寫起來很輕鬆:面試
<button @click="clickHandler">click me!</button>
React 除了語法糖外,底層還將 DOM 事件再封裝一層,並幫你全都代理到 document
上,性能很不錯:segmentfault
<button onClick={clickHandler}>click me!</button>
固然無論是什麼框架,底層都等同於經過 Javascript 進行操做:瀏覽器
document.querySelector('#id').addEventListener('click', clickHandler)
前面說到 React 會幫你把事件代理到 document 上,這是什麼意思呢?服務器
看這個的 簡單小例子,點擊按鈕新增 li
時,會一併註冊事件監聽:微信
<!--HTML--> <button id="push">push</button> <button id="pop">pop</button> <ul id="list"></ul>
/*JavaScript*/ (function() { document.querySelector('#push').addEventListener('click', pushHandler) document.querySelector('#pop').addEventListener('click', popHandler) const list = document.querySelector('#list') function pushHandler() { list.appendChild(getNewElem(list.childNodes.length)) } function popHandler() { document.querySelectorAll('#list>li')[list.childNodes.length - 1].remove() } function getNewElem(text) { const elem = document.createElement('li') elem.innerText = text elem.addEventListener('click', eventHandler) return elem } function eventHandler(e) { alert(e.target.innerText) } })()
這樣很直觀,但缺點也很明顯;每新增一個元素,都會建立一個事件監聽,當數量增多,形成的內存消耗也會十分可觀:
function pushHandler() { list.appendChild(getNewElem(list.childNodes.length)) }function getNewElem(text) { const elem = document.createElement('li') elem.innerText = text elem.addEventListener('click', () => alert(text)) return elem }
若是把事件監聽註冊在外層的 ul
,並在點擊事件觸發時判斷觸發到到的是誰:
function listClickHandler(e){ if (e.target.tagName === 'LI') alert(e.target.innerText) }
經過事件代理,不管內容有多少,事件監聽都只會有一組,效能獲得了很大的提高。
註冊事件監聽器很方便,但在肯定不會再使用監聽器時,要記得經過 removeEventListener
將事件監聽移除。若是留下了無用的事件監聽器,將會形成內存的浪費,對性能有很大的損害。
你們應該注意到了,在前面那個簡易的小例子中並無移除事件監聽,並且每建立一個新的子元素,都會同時建立新的函數:
function getNewElem(text) { const elem = document.createElement('li') elem.innerText = text // 在這裏建立新的匿名函數 elem.addEventListener('click', () => alert(text)) return elem }
比較好的寫法是把匿名函式抽出來,並在移除子元素時一併移除事件監聽器:
function popHandler() { const elem = document.querySelectorAll('#list>li')[list.childNodes.length - 1] elem.removeEventListener('click', eventHandler) // 移除事件監聽 elem.remove() }function getNewElem(text) { const elem = document.createElement('li') elem.innerText = text elem.addEventListener('click', eventHandler) return elem }function eventHandler(e) { alert(e.target.innerText) }
在 Vue 和 React 等主流網頁框架中,只要是使用內建的語法註冊的事件監聽,它們都會自動在無用的時候移除,能夠放心使用;若是是本身實現事件監聽,務必要記得移除。
跑題太遠了,因此到底什麼是捕獲與冒泡?
瀏覽器中的事件傳遞過程分紅三個階段:
這就是剛剛提到的事件代理的機制了;在事件傳遞過程當中,捕獲冒泡階段必然會通過外層元素,所以能夠將事件監聽註冊到外層元素上。
另外,當咱們在用 addEventListener
註冊事件監聽器時,能夠傳遞第三個參數,指定這個事件要在什麼階段觸發:
elem.addEventListener('click', eventHandler) // 未指定,預設爲冒泡 elem.addEventListener('click', eventHandler, false) // 冒泡 elem.addEventListener('click', eventHandler, true) // 捕獲 elem.addEventListener('click', eventHandler, { capture: true // 是否爲捕獲。 IE、Edge 不支援。其餘屬性請參考 MDN })
如上圖所示, 當一個 DOM 事件發生時,會由最外層的 window
開始依次向內傳遞事件,一直傳到咱們的事件目標,觸發完目標上註冊的事件監聽,再進入冒泡階段反向傳遞;由指定觸發的階段,就能肯定執行的順序了。