系統(tǒng)理解瀏覽器之事件機(jī)制
事件流
在早期 IE 和 Netscape 團(tuán)隊在開發(fā)第四代瀏覽器的時候,遇到一個問題:當(dāng)點(diǎn)擊一個按鈕的時候,是應(yīng)該先處理父級的事件呢?還是應(yīng)該先處理按鈕的事件呢?IE 和 Netscape 給出了 2 種完全相反的答案,IE 提出事件冒泡的概念,而 Netscape 則支持事件捕獲。
事件冒泡
事件冒泡認(rèn)為事件應(yīng)該由最具體的元素開始觸發(fā),然后層層往父級傳播:
事件捕獲
而事件捕獲則相反,認(rèn)為最外層的元素應(yīng)該最先收到事件,然后層層往下級傳遞:

DOM 事件流
為了在瀏覽器中兼容這 2 種事件流,在 DOM2 Events 規(guī)范中將事件流分為 3 個階段:事件捕獲階段、到底目標(biāo)階段、事件冒泡階段。
可以通過指定 addEventListener 的第三個參數(shù)為 true 來設(shè)置事件是在捕獲階段調(diào)用事件處理程序,默認(rèn)是 false 指在冒泡階段調(diào)用事件處理程序。
- 所有現(xiàn)代瀏覽器都支持 DOM 事件流,只有 IE8 及更早版本不支持。
事件處理程序
HTML 事件處理程序
就是將事件處理程序直接綁定到 HTML 的屬性中:
- // 方式一
- <div onclick="console.log('hello world')"></div>
- 方式二
- <div onclick="print(event)"></div>
- <script>
- function print(e) { }
- </script>
HTML 事件處理程序修改事件相對麻煩,可能需要同時修改 HTML 和 JS,所以大家都不愛使用這種方式綁定事件。
DOM0 事件處理程序
將一個函數(shù)賦值給 DOM 元素的一個事件處理程序?qū)傩?,比?onclick:
- let btn = document.getElementById('div')
- // 添加事件
- btn.onclick = function() { }
- // 移除事件
- btn.onclick = null
DOM2 事件處理程序
通過 addEventListener 可以添加 DOM2 級別的事件處理程序,它接收 3 個參數(shù):事件名、事件處理程序和 useCapture (它是一個可選參數(shù),是個布爾值,默認(rèn)為 false 表示在冒泡階段調(diào)用事件處理程序)
- let btn = document.getElementById('div')
- btn.addEventListener('click', () => {
- }, false)
和 DOM0 事件處理程序的區(qū)別:
- addEventListener 可以改變事件流,即可以在捕獲階段觸發(fā)事件,而 DOM0 是不行的;
- addEventListener 可以為同一個元素多次添加同一類型的事件處理程序,先添加的事件處理程序會先觸發(fā),而 DOM0 如果給同一個元素綁定多個相同類型的事件處理程序的話,則后面添加的會覆蓋前面定義的;
它有幾個注意事項:
- 如果不需要在捕獲階段進(jìn)行攔截操作,則 useCapture 即第三個參可以不傳;
- 通過 addEventListener 添加的事件處理程序只能通過 removeEventListener 移除,而且綁定的事件處理程序必須是同一個。
- let btn = document.getElementById('div')
- let handler = function() { }
- btn.addEventListener("click", handler)
- btn.removeEventListener("click", handler)
事件處理函數(shù)
由于 addEventListener 無法兼容 IE8 及更早版本,所以此時就可以使用 attachEvent 添加事件處理程序和用 detachEvent 移除事件處理程序。
- let btn = document.getElementById('div')
- btn.attachEvent("onclick", function() { })
它有這么幾個注意事項:
- 注冊的事件名和 DOM0 一樣,需要帶上 on,比如 onclick;
- 在通過 attachEvent 添加的事件處理程序內(nèi)部 this 會指向 window,而 DOM0 和 DOM2的 this 會指向元素本身;
- 和 addEventListener 一樣, attachEvent 也可以針對同一元素多次添加同一個事件類型的處理程序,但是觸發(fā)順序是后定義的先觸發(fā);
- 通過 detachEvent 移除事件處理程序的時候,處理函數(shù)必須是和注冊的同一個,這點(diǎn)和 addEventListener 保持一致;
attachEvent 和 detachEvent 是 IE 專屬的 API,所以如果有兼容性要求,我們可以寫出跨瀏覽器的事件處理程序:
- var EventUtil = {
- addHandler: function(element, type, handler) {
- if (element.addEventListener) {
- element.addEventListener(type, handler, false)
- } else if (element.attachEvent) {
- element.attachEvent("on" + type, handler)
- } else {
- element["on" + type] = handler;
- }
- },
- removeHandler: function(element, type, handler) {
- if (element.removeEventListener) {
- element.removeEventListener(type, handler, false)
- } else if (element.detachEvent) {
- element.detachEvent("on" + type, handler)
- } else {
- element["on" + type] = null
- }
- }
- }
事件對象
通過不同的事件處理程序添加的事件,event 對象的屬性略有不同,我們不需要記住他們的差異,只需要在平時寫代碼的時候養(yǎng)成一個寫兼容代碼的習(xí)慣即可,如下是一個兼容各種 event 對象的事件處理程序:
- let handler = function(event) {
- // 事件對象
- let event = event || window.event
- // 目標(biāo)元素
- let target = event.target || event.srcElement
- // 阻止默認(rèn)事件觸發(fā)
- if (event.preventDefault) {
- event.preventDefault()
- } else {
- event.returnValue = false
- }
- // 阻止事件冒泡
- if (event.stopPropagation) {
- event.stopPropagation()
- } else {
- event.cancelBubble = true
- }
- }
事件類型
DOM3 Events 定義了如下事件類型:
- 用戶界面事件(UIEvent):涉及與 BOM 交互的通用瀏覽器事件,比如 onload、resize、scroll、input、select 等;
- 焦點(diǎn)事件(FocusEvent):在元素獲得和失去焦點(diǎn)時觸發(fā),比如 focus、blur;
- 鼠標(biāo)事件(MouseEvent):使用鼠標(biāo)在頁面上執(zhí)行某些操作時觸發(fā),比如 click、mousedown、mouseover 等;
- 滾輪事件(WheelEvent):使用鼠標(biāo)滾輪(或類似設(shè)備)時觸發(fā),比如 mousewheel;
- 輸入事件(InputEvent):向文檔中輸入文本時觸發(fā),比如 textInput;
- 鍵盤事件(KeyboardEvent):使用鍵盤在頁面上執(zhí)行某些操作時觸發(fā),比如 keydown、keypress;
- 合成事件(CompositionEvent):在使用某種 IME(Input Method Editor,輸入法編輯器)輸入字符時觸發(fā),比如 compositionstart。
事件委托
事件委托是指將多個元素上綁定的事件通過利用事件冒泡的原理從而轉(zhuǎn)移到他們共同的父級上去綁定,從而在一定程度上起到性能優(yōu)化的作用,有的人也喜歡叫它事件代理。比如在 Vue中經(jīng)常會將事件綁定到每個列表項中:
- <ul>
- <li v-for="item in list" :key="item" @click="handleClick(item)">{{item}}</li>
- </ul>
其實(shí)更好的做法是利用事件委托,將事件綁定到 ul 上:
- <ul @click="handleClick">
- <li v-for="item in list" :key="item" :data-item="item">{{item}}</li>
- </ul>
- handleClick(event) {
- let target = event.target
- if (target === 'li') {
- let data = target.dataset.item
- }
- }
感謝閱讀首先感謝你閱讀本文,相信你付出的時間值得擁有這份回報。