本文首發自JohnieXu's Blog,轉載請註明出處😁javascript
在網頁端、移動端H五、小程序等各個終端環境的前端開發中隨處可見事件的運用,可見事件機制的確是前端這一塊的重中之重。通過我研讀大量博客文章以及開源框架源碼後,此次算是對DOM事件機制有了更新更全面的認識。html
下文主要經過一個例子帶你分析DOM事件的傳遞、事件處理,而後在此基礎上再深刻總結對事件監聽的優化方案,最終總結下DOM事件機制在開源框架中的實戰應用。前端
如下面這個普通的html文件爲例vue
<!DOCTYPE html>
<html lang="en" onclick="handleClickHtml()">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body onclick="handleBodyClick()">
<div id="div1" onclick="handleClick2()">
<button id="button1" onclick="handleClick1()">handleClick1</button>
</div>
<script> function handleClick1(e) { console.log('click1') } function handleClick2(e) { console.log('click2') } function handleBodyClick(e) { console.log('body clicked') } function handleClickHtml(e) { console.log('html clicked') } document.addEventListener('click', e => { console.log('document clicked') }) </script>
</body>
</html>
複製代碼
例子中給document、html、body、div、button
都添加了點擊事件,其執行結果以下:java
事件產生後,從window對象自上而下向目標節點傳遞,抵達目標節點後會沿着相反方向傳遞node
由鼠標、鍵盤、觸摸屏、窗口縮放、圖片加載、節點焦點變化等觸發事件,每一個觸發的事件都有一個target——事件目標,根據target往html根節點遍歷就能夠肯定節點嵌套層級關係,從而肯定此事件的傳遞路徑。react
事件傳遞路徑由瀏覽器、webview來肯定git
一個事件來回走兩遍是便於對事件響應時機的控制(下文的事件冒泡與事件捕獲的應用中有體現),同時也存在事件冒泡的瀏覽器歷史緣由(與IE、Netscape有關,不贅述)。github
事件在從自上而下的傳遞過程當中會判斷當前node節點是否綁定對應事件的監聽器,如有則執行響應監聽器的事件處理程序(也就是綁定的事件要執行的邏輯),這裏的執行就是所謂的事件響應。web
事件的響應過於與事件的類型是捕獲事件、冒泡事件有關,同時是否組件冒泡還會影響該事件的後續傳遞過程,具體執行流程總結成以下圖所示:
根據事件傳遞中的兩種傳遞路徑分別分爲:由上至下——事件捕獲階段,由下至上——事件冒泡階段,指的是一個事件由window(上圖並未標出window)到target之間的傳遞的過程。
能夠經過阻止事件冒泡讓事件再也不繼續完成由下至上的過程,這會讓target的父節點綁定冒泡階段的事件監聽器不響應事件。
能夠經過指定事件在捕獲階段執行。
看下面這個例子,能夠經過stopPropagation
來阻止事件繼續往上冒泡,window、document、html
上添加的點擊事件均不會生效
<!-- 省略了部分代碼 -->
<div id="div1">
<button id="button1">button</button>
</div>
<script> var div1 = document.getElementById('div1') var button1 = document.getElementById('button1') div1.addEventListener('click', (e) => { console.log(e.currentTarget) }, false) button1.addEventListener('click', (e) => { console.log(e.currentTarget) e.stopPropagation() // 關鍵代碼:阻止了click事件繼續往上冒泡 }, false) 複製代碼// 如下是監聽html、document的點擊事件 function handleClickHtml(e) { console.log('html clicked') } document.addEventListener('click', e => { console.log('document clicked') }) </script> 複製代碼
點擊button的打印結果以下:
addEventListener須要用attachEvent進行兼容性處理,第三個參數默認值爲false表示在事件冒泡階段響應事件
仍是上面這個例子,只更改div1部分的事件監聽,的第三個參數爲true來開啓事件捕獲
<!-- 省略了部分代碼 -->
<div id="div1">
<button id="button1">button</button>
</div>
<script> var div1 = document.getElementById('div1') var button1 = document.getElementById('button1') div1.addEventListener('click', (e) => { console.log(e.currentTarget) }, true) // true:表示div1在事件的捕獲階段響應事件 button1.addEventListener('click', (e) => { console.log(e.currentTarget) e.stopPropagation() // 阻止事件冒泡 }, false) 複製代碼// 如下是監聽html、document的點擊事件 function handleClickHtml(e) { console.log('html clicked') } document.addEventListener('click', e => { console.log('document clicked') }) </script> 複製代碼
點擊button的打印結果以下:
依次執行了div1和button1綁定的點擊事件,注意這裏二者的執行順序很關鍵。事件在捕獲階段遇到div1節點時執行打印,而後在button處(處於目標階段)執行打印,而後事件被阻止冒泡再也不往上傳遞。
考慮如下場景:
有一個長消息列表實時接受新的消息,滾動到底部時加載更多消息,點擊消息可進入會話窗口頁面,左滑刪除此條消息
問題:
如何給此消息列表添加左滑事件?
抽象來看這個問題就是如何高效的給大批量(甚至是無限量)節點添加事件,這裏暫時不考慮vue、react等框架
每註冊一個事件監聽器都須要使用必定內存,同時在dom節點多了以後事件通過的捕獲、冒泡階段要處理更多DOM節點,事件傳遞也就不夠高效了
父節點嵌套的子節點的觸發的事件會經過事件冒泡到達父節點,事件處理程序不直接綁定到子節點,統一由公共父節點進行事件監聽。
e.target表示觸發事件的元素,經過e.target能夠判斷具體響應那個元素的事件 e.currentTarget表示事件綁定的元素,在事件委託狀況下指向同一個元素
<!-- 省略了部分代碼 -->
<div id="div1">
<button id="button1">button</button>
</div>
<script> document.addEventListener('click', e => { // e.target:觸發事件的元素 // e.currentTarget:事件監聽器綁定的元素 // console.log(e.target, e.currentTarget) if (e.target.id === 'button1') { return console.log('button1 clicked') } if (e.target.id === 'div1') { return console.log('div1 clicked') } if (e.target.tagName === 'HTML') { return console.log('html clicked') } if (e.target.tagName === 'BODY') { return cosnole.log('body clicked') } }) </script>
複製代碼
依次點擊html、div一、button1
會依次打印結果:
上面事件委託能夠解決大批量節點事件監聽的性能問題,可是DOM節點量過大致使界面滾動不流暢的問題沒解決。
虛擬滾動是指在列表滾動事件中動態計算出當前窗口中可以顯示的列表節點,其先後窗口不顯示的節點各用一個div進行佔位處理,並調整佔位div的樣式確模擬的列表樣式與原始列表無異處。
虛擬滾動已是一個完美解決長列表渲染性能的方案,針對react、vue均有對應庫實現,此處再也不贅述
事件委託也叫事件代理,在react中的合成事件則是徹底採用了事件代理
基於react@16的ant-design-pro
項目的事件綁定截圖以下:
直接經過onClick綁定的事件經過react處理最終會統一綁定到document上,這正是使用的事件委託。
最新版微信小程序中綁定事件有兩種類型:bind:methodName、catch:methodName、mut-bind:methodName、mut-catch:methodName
,其中區別最大的是前二者。
methodName表示移動端事件名,例如:tap、touchmove等
bind:methodName
採用冒泡綁定事件,catch:methodName
**採用冒泡綁定事件並阻止冒泡
實現思路:彈框採用一層遮罩層overlay遮蓋,遮罩層寬高設爲與窗口寬高同大小,採用catch:touchmove
綁定該層的touchmove事件爲空函數阻止其冒泡。
給overlay組件外層綁定touchmove爲空函數並阻止冒泡:
// overlay/index.wxml
<van-transition
show="{{ show }}"
custom-class="van-overlay"
custom-style="z-index: {{ zIndex }}; {{ customStyle }}"
duration="{{ duration }}"
bind:tap="onClick"
catch:touchmove="noop"
/* * 經過catch:touchmove來阻止冒泡至overlay層的事件繼續往上冒泡, * 實現內層嵌套的內容能夠響應touchmove事件而外層(頁面)滾動事件不響應), * 以此來解決滾動事件穿透 */
>
<slot></slot>
</van-transition>
複製代碼
overlay組件外層fixed定位固定寬高防止超高出現滾動條:
// overlay/index.less
.van-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
.theme(background-color, '@overlay-background-color');
}
複製代碼
實現思路:與vant-weapp小程序框架中類似,彈框採用一層遮罩層overlay遮蓋,遮罩層寬高設爲與窗口寬高同大小,採用@touchmove
綁定該層的touchmove事件,在事件中用event.stopPropagation阻止事件冒泡。
給overlay組件外層綁定touchmove事件阻止冒泡:
return (
<transition name="van-fade">
<div
vShow={props.show}
style={style}
class={[bem(), props.className]}
onTouchmove={preventTouchMove}
{...inherit(ctx, true)}
/>
</transition> ); 複製代碼const preventTouchMove(e: Event): void = () => { event.stopPropagation(); } 複製代碼
overlay組件外層fixed定位固定寬高防止超高滾動條:
.van-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
.theme(background-color, '@overlay-background-color');
}
複製代碼
既然看到這裏不妨點個贊鼓勵下做者唄 :)
做者博客:blog.lessing.online/
做者github:github.com/johniexu