JS事件流和事件委託

codeXiu發表於2018-12-17

在上一篇《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');
}
複製程式碼

JS事件流和事件委託
當我們點選box1時都知道輸出box1,可是當我們點選box3時彈出什麼呢?

JS事件流和事件委託
你可能會感覺奇怪,為什麼我點選的box3怎麼其他的也會觸發?因為事件冒泡。那什麼是事件冒泡呢?概念請自行百度,直接上圖。

JS事件流和事件委託
這就叫做事件冒泡,一級一級往上冒直到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看到

JS事件流和事件委託
你可能會發現順序反過來了,那這是為什麼呢?因為事件捕獲,那什麼是時間捕獲呢?概念請自行百度,直接上圖。

JS事件流和事件委託
這就是捕獲階段,跟冒泡階段完全相反。

那冒泡跟捕獲的執行順序是什麼樣的呢?我分別在每一個元素上繫結了兩個階段的同一事件,我們來看看觸發的順序。

<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看到,先捕獲後冒泡。

JS事件流和事件委託
那是不是都這樣呢?我們稍微改動一下。

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的冒泡階段後面
複製程式碼

看看觸發的順序是不是還一樣呢?

JS事件流和事件委託
發現反過來了,其實這就叫做目標階段吧。在你觸發事件的目標元素身上不區分冒泡捕獲,按繫結的順序來執行。

我們用圖來看一下。

JS事件流和事件委託
這樣是不是太簡單我們來一點複雜的。

<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');
}
複製程式碼

觸發順序是什麼樣的?(我覺得你最好先自己把答案寫出來)

看看你答對了沒有

JS事件流和事件委託

這樣會不會太簡單,換一下順序

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); 
複製程式碼

JS事件流和事件委託
答對了嗎?

  • 再說一下冒泡和捕獲
<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);
複製程式碼

JS事件流和事件委託
你說這樣點選會輸出什麼?你是不是猶豫了?說明你還是不懂冒泡和捕獲。

不要讓你看到的騙了你,冒泡是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')
})
複製程式碼

JS事件流和事件委託

box1.attachEvent('onclick', function (){
	console.log('box1')
})
box1.attachEvent('onclick', function (){
	console.log('box2')
})
box1.attachEvent('onclick', function (){
	console.log('box3')
})
複製程式碼

這個會輸出什麼?(提示:不會覆蓋) 答案是:box1 box2 box3 哈哈,開玩笑啊。

JS事件流和事件委託
是不是很奇怪,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)
複製程式碼

JS事件流和事件委託
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);
	}
}
複製程式碼

評論區有位大佬指出了我的錯誤,因為這樣形成了閉包,得不到輸出的結果。因為當時只想演示事件委託沒有注意到這個情況,在此為讀者們表示深深的歉意。下面是更改之後的,之所以不把錯誤的修改是想告誡自己還有和我犯同樣錯誤的人。

let li = document.getElementsByTagName("li");
//ES6 
for (let i = 0; i < li.length; i++) {
	li[i].onclick = ()=>{
		console.log(i);
	}
}
//IIFE(1)
for (var i = 0; i < li.length; i++) {
	(function(j){
		li[j].onclick = ()=>{
		console.log(j);
	}})(i)
}
//IIFE(2)
for (var i = 0; i < li.length; i++) {
	li[i].onclick = (function(j){
		return ()=>{
			console.log(j);
		}
	})(i);
}
複製程式碼

這樣做是對的單不夠好,要是再加幾個li或有很多的100|1000個li你還這樣做是不是感覺就不好了。我們就需要為每一個li註冊事件,麻煩不說,註冊很多事件就不好。那麼我們怎麼辦的,使用事件委託,就是把你的事件委託給別人(父級),利用事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。

我們來看一下。

ul.onclick = function (e){
	console.log(e.target);
}
複製程式碼

這就是事件委託。

  • 優點:
    • 可以大量節省記憶體佔用,減少事件註冊。
    • 可以實現當新增子物件時,無需再對其進行事件繫結,對於動態內容部分尤為合適
  • 缺點:
    • 事件代理的常用應用應該僅限於上述需求,如果把所有事件都用事件代理,可能會出現事件誤判。即本不該被觸發的事件被繫結上了事件。

相關文章