JS事件流和事件委託
在上一篇《JS知識點大雜燴》中說到了事件流但沒有詳細的介紹,這篇文章就來介紹一下事件流。
事件流一共由三個階段分別是:
1.捕獲階段 2.目標階段 3.冒泡階段 複製程式碼
事件繫結大家都知道,有DOM0級(on+type)和DOM2級(addEventListener),我覺得說那麼多概念不好理解,直接看程式碼吧,為了方便我就直接使用id來獲取元素。
- DOM0級
<div id="box1"></div> box1.onclick = function(){ console.log('box1'); } 複製程式碼
輸出了 box1
這個我們都知道,再來看一下。
<div id="box1"></div> box1.onclick = function(){ console.log('box1'); } box1.onclick = function(){ console.log('box1 two'); } 複製程式碼
輸出了 box1 two
,因為DOM0級會覆蓋掉之前在同一元素上面的繫結,再來看一下。
<div id="box1"> <div id="box2"> <div id="box3"></div> </div> </div> box1.onclick = function(){ console.log('box1'); } box2.onclick = function(){ console.log('box2'); } box3.onclick = function(){ console.log('box3'); } 複製程式碼
當我們點選box1時都知道輸出box1,可是當我們點選box3時彈出什麼呢?
你可能會感覺奇怪,為什麼我點選的box3怎麼其他的也會觸發?因為 事件冒泡。那什麼是事件冒泡呢?概念請自行百度,直接上圖。
這就叫做事件冒泡,一級一級往上冒直到window這裡我沒有畫出來。DOM0級只支援冒泡階段。
- DOM2級
<div id="box1"> <div id="box2"> <div id="box3"></div> </div> </div> box1.addEventListener('click', function(){ console.log('box1'); },false); box2.addEventListener('click', function(){ console.log('box2'); },false); box3.addEventListener('click', function(){ console.log('box3'); },false); 複製程式碼
輸出跟上面是一樣的,因為我們繫結在了冒泡階段。(true捕獲,false冒泡)。 我們再來看看捕獲階段是怎麼樣的
<div id="box1"> <div id="box2"> <div id="box3"></div> </div> </div> box1.addEventListener('click', function(){ console.log('box1'); },true); box2.addEventListener('click', function(){ console.log('box2'); },true); box3.addEventListener('click', function(){ console.log('box3'); },true); 複製程式碼
我們點選box3看到
你可能會發現順序反過來了,那這是為什麼呢?因為 事件捕獲,那什麼是時間捕獲呢?概念請自行百度,直接上圖。
這就是捕獲階段,跟冒泡階段完全相反。
那冒泡跟捕獲的執行順序是什麼樣的呢?我分別在每一個元素上綁定了兩個階段的同一事件,我們來看看觸發的順序。
<div id="box1"> <div id="box2"> <div id="box3"></div> </div> </div> box1.addEventListener('click', function(){ console.log('box1 捕獲階段'); },true); box2.addEventListener('click', function(){ console.log('box2 捕獲階段'); },true); box3.addEventListener('click', function(){ console.log('box3 捕獲階段'); },true); box1.addEventListener('click', function(){ console.log('box1 冒泡階段'); },false); box2.addEventListener('click', function(){ console.log('box2 冒泡階段'); },false); box3.addEventListener('click', function(){ console.log('box3 冒泡階段'); },false); 複製程式碼
我們點選box3看到,先捕獲後冒泡。
那是不是都這樣呢?我們稍微改動一下。
box1.addEventListener('click', function(){ console.log('box1 捕獲階段'); },true); box2.addEventListener('click', function(){ console.log('box2 捕獲階段'); },true); box1.addEventListener('click', function(){ console.log('box1 冒泡階段'); },false); box2.addEventListener('click', function(){ console.log('box2 冒泡階段'); },false); box3.addEventListener('click', function(){ console.log('box3 冒泡階段'); },false); box3.addEventListener('click', function(){ console.log('box3 捕獲階段'); },true);// 將box3的捕獲階段放到box3的冒泡階段後面 複製程式碼
看看觸發的順序是不是還一樣呢?
發現反過來了,其實這就叫做 目標階段吧。在你觸發事件的目標元素身上不區分冒泡捕獲,按繫結的順序來執行。
我們用圖來看一下。
這樣是不是太簡單我們來一點複雜的。
<div id="box1"> <div id="box2"> <div id="box3"></div> </div> </div> box1.addEventListener('click', function(){ console.log('box1 捕獲階段'); },true); box2.addEventListener('click', function(){ console.log('box2 捕獲階段'); },true); box3.addEventListener('click', function(){ console.log('box3 捕獲階段'); },true); box1.addEventListener('click', function(){ console.log('box1 冒泡階段'); },false); box2.addEventListener('click', function(){ console.log('box2 冒泡階段'); },false); box3.addEventListener('click', function(){ console.log('box3 冒泡階段'); },false); box1.onclick = function () { console.log('box1 51561'); } box2.onclick = function () { console.log('box2'); } box3.onclick = function () { console.log('box3'); } box1.onclick = function () { console.log('box1'); } 複製程式碼
觸發順序是什麼樣的?(我覺得你最好先自己把答案寫出來)
看看你答對了沒有
這樣會不會太簡單,換一下順序
box1.onclick = function () { console.log('box1 51561'); } box2.onclick = function () { console.log('box2'); } box3.onclick = function () { console.log('box3'); } box1.onclick = function () { console.log('box1'); } box1.addEventListener('click', function(){ console.log('box1 捕獲階段'); },true); box2.addEventListener('click', function(){ console.log('box2 捕獲階段'); },true); box1.addEventListener('click', function(){ console.log('box1 冒泡階段'); },false); box2.addEventListener('click', function(){ console.log('box2 冒泡階段'); },false); box3.addEventListener('click', function(){ console.log('box3 冒泡階段'); },false); box3.addEventListener('click', function(){ console.log('box3 捕獲階段'); },true); 複製程式碼
答對了嗎?
- 再說一下冒泡和捕獲
<div id="box1"> <div id="box2"> <div id="box3"></div> </div> </div> box1.onclick = function () { console.log('box1 51561'); } box2.onclick = function () { console.log('box2'); } box3.onclick = function () { console.log('box3'); } box1.onclick = function () { console.log('box1'); } box1.addEventListener('click', function(){ console.log('box1 捕獲階段'); },true); box2.addEventListener('click', function(){ console.log('box2 捕獲階段'); },true); box1.addEventListener('click', function(){ console.log('box1 冒泡階段'); },false); box2.addEventListener('click', function(){ console.log('box2 冒泡階段'); },false); box3.addEventListener('click', function(){ console.log('box3 冒泡階段'); },false); box3.addEventListener('click', function(){ console.log('box3 捕獲階段'); },true); 複製程式碼
你說這樣點選會輸出什麼?你是不是猶豫了?說明你還是不懂冒泡和捕獲。
不要讓你看到的騙了你,冒泡是DOM結構的父子關係而不是看起來是不是包裹的關係。(答案同上面)。
- 忘了IE了,說一下IE的事件機制。
IE上面不支援addEventListener但是它有attachEvent
box1.onclick = function () { console.log('box1 51561'); } box2.onclick = function () { console.log('box2'); } box3.onclick = function () { console.log('box3'); } box1.onclick = function () { console.log('box1'); } box1.attachEvent('onclick', function (){ console.log('box1 attachEvent') }) box2.attachEvent('onclick', function (){ console.log('box2 attachEvent') }) box3.attachEvent('onclick', function (){ console.log('box3 attachEvent') }) 複製程式碼
box1.attachEvent('onclick', function (){ console.log('box1') }) box1.attachEvent('onclick', function (){ console.log('box2') }) box1.attachEvent('onclick', function (){ console.log('box3') }) 複製程式碼
這個會輸出什麼?(提示:不會覆蓋) 答案是:box1 box2 box3 哈哈,開玩笑啊。
是不是很奇怪,IE中該事件是先繫結的後輸出。IE6、7、8,不支援事件捕獲只支援事件冒泡。
- 阻止事件冒泡相容
我們先來說一下不支援冒泡的事件:blur、focus、mouseenter、mouseleave。(我就知道這些) 還是這個例子,我們看一下阻止冒泡。
box1.onclick = function (){ console.log('box1') } box2.onclick = function (){ console.log('box2') } box3.onclick = function (e){ e.stopPropagation(); console.log('box3') } 複製程式碼
只輸出了box3. 雖然阻止了冒泡但在IE8及以下是不好使的,我們看一下相容的寫法。
function stopPropagate(e){ var event = e || window.event; if(event.stopPropagation){ event.stopPropagation(); }else if(event.cancelBubble){ //IE event.cancelBubble = true; } } 複製程式碼
阻止預設事件相容
function preventDef(e){ var g = e || window.event; if(g.preventDefault){ g.preventDefault(); }else if(g.returnValue){ g.returnValue = false; } return false; } 複製程式碼
我們再來看看事件繫結的this指向
box1.onclick = function (){ console.log('onclick', this); } box1.addEventListener('click',function () { console.log('addEventListener', this); }, false) 複製程式碼
IE6、7、8,事件繫結的this指向
box1.attachEvent('onclick',function () { console.log('attachEvent', this); }) 複製程式碼
attachEvent [object Window]
我們發現,IE6、7、8 this指向window
- 擴充套件 事件委託
<ul id="ul"> <li>1</li> <li>2</li> <li>3</li> </ul> 複製程式碼
如果我們要監聽每一個li的行為,你會不會這麼做。
let li = document.getElementsByTagName("li"); for (var i = 0; i < li.length; i++) { li.onclick = ()=>{ console.log(i); } } 複製程式碼
這樣做是對的單不夠好,要是再加幾個li或有很多的100|1000個li你還這樣做是不是感覺就不好了。我們就需要為每一個li註冊事件,麻煩不說,註冊很多事件就不好。那麼我們怎麼辦的,使用 事件委託 ,就是把你的事件委託給別人(父級),利用事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。
我們來看一下。
ul.onclick = function (e){ console.log(e.target); } 複製程式碼
這就是事件委託。
- 優點:
- 可以大量節省記憶體佔用,減少事件註冊。
- 可以實現當新增子物件時,無需再對其進行事件繫結,對於動態內容部分尤為合適
- 缺點:
- 事件代理的常用應用應該僅限於上述需求,如果把所有事件都用事件代理,可能會出現事件誤判。即本不該被觸發的事件被繫結上了事件。