1. 程式人生 > >JS事件模型——監聽函式、事件代理、事件傳播——20181116

JS事件模型——監聽函式、事件代理、事件傳播——20181116

1、監聽函式

瀏覽器的事件模型,就是通過監聽函式(listener)對事件做出反應。事件發生後,瀏覽器監聽到了這個事件,就會執行對應的監聽函式。這是事件驅動程式設計模式(event-driven)的主要程式設計方式。

JavaScript 有三種方法,可以為事件繫結監聽函式。

1.1 HTML 的 on- 屬性

HTML 語言允許在元素的屬性中,直接定義某些事件的監聽程式碼。

1.2 元素節點的事件屬性

元素節點物件的事件屬性,同樣可以指定監聽函式。

1.3 EventTarget.addEventListener()

所有 DOM 節點例項都有addEventListener

方法,用來為該節點定義事件的監聽函式。

<!DOCTYPE html>
<html>
<head>
</head>

<body>
	<input type="button" id="btn" value="btn">
    <p id="para">Hello</p>
    <div><p id="try">try</p></div>
    <!-- HTML 語言允許在元素的屬性中,直接定義某些事件的監聽程式碼。 -->
    <div onclick="console.log('觸發')">觸發</div> 
    

<script>
	//元素節點物件的事件屬性,同樣可以指定監聽函式。使用這個方法指定的監聽函式,也是隻會在冒泡階段觸發。
	document.getElementById('try').onclick = function (event) {
 	 console.log('觸發事件');
	};

	//所有 DOM 節點例項都有addEventListener方法,用來為該節點定義事件的監聽函式。
	function hello() {
 	 console.log('Hello world');
	}
	var button = document.getElementById('btn');
	button.addEventListener('mouseover', hello);
  	
	var para = document.getElementById('para');
	para.addEventListener('click', function () {
  	console.log(this.nodeName); // "P"
	}, false);
 	var event = new Event('click');
	para.dispatchEvent(event); 
    var canceled = !para.dispatchEvent(event);
	if (canceled) {
  		console.log('事件取消');
	} else {
  		console.log('事件未取消');
	}
</script>
</body>
</html>

1.4 小結 

上面三種方法,第一種“HTML 的 on- 屬性”,違反了 HTML 與 JavaScript 程式碼相分離的原則,將兩者寫在一起,不利於程式碼分工,因此不推薦使用。

第二種“元素節點的事件屬性”的缺點在於,同一個事件只能定義一個監聽函式,也就是說,如果定義兩次onclick屬性,後一次定義會覆蓋前一次。因此,也不推薦使用。

第三種EventTarget.addEventListener是推薦的指定監聽函式的方法。它有如下優點:

  • 同一個事件可以新增多個監聽函式。
  • 能夠指定在哪個階段(捕獲階段還是冒泡階段)觸發監聽函式。
  • 除了 DOM 節點,其他物件(比如window
    XMLHttpRequest等)也有這個介面,它等於是整個 JavaScript 統一的監聽函式介面。

2、事件的傳播

一個事件發生後,會在子元素和父元素之間傳播(propagation)。這種傳播分成三個階段。

  • 第一階段:從window物件傳導到目標節點(上層傳到底層),稱為“捕獲階段”(capture phase)。
  • 第二階段:在目標節點上觸發,稱為“目標階段”(target phase)。
  • 第三階段:從目標節點傳導回window物件(從底層傳回上層),稱為“冒泡階段”(bubbling phase)。

這種三階段的傳播模型,使得同一個事件會在多個節點上觸發。

<!DOCTYPE html>
<html>
<head>
</head>

<body>
  <div>
  <p>點選</p>
 </div>

<script>
	var phases = {
  1: 'capture',
  2: 'target',
  3: 'bubble'
};

var div = document.querySelector('div');
var p = document.querySelector('p');

div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);

function callback(event) {
  var tag = event.currentTarget.tagName;
  console.log(event.currentTarget.tagName,event.eventPhase);
  var phase = phases[event.eventPhase];
  console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}

// 點選以後的結果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'
</script>
</body>
</html>

上面程式碼中,<div>節點之中有一個<p>節點。

如果對這兩個節點,都設定click事件的監聽函式(每個節點的捕獲階段和監聽階段,各設定一個監聽函式),共計設定四個監聽函式。然後,對<p>點選,click事件會觸發四次。

上面程式碼表示,click事件被觸發了四次:<div>節點的捕獲階段和冒泡階段各1次,<p>節點的目標階段觸發了2次。

  1. 捕獲階段:事件從<div><p>傳播時,觸發<div>click事件;
  2. 目標階段:事件從<div>到達<p>時,觸發<p>click事件;
  3. 冒泡階段:事件從<p>傳回<div>時,再次觸發<div>click事件。

其中,<p>節點有兩個監聽函式(addEventListener方法第三個引數的不同,會導致繫結兩個監聽函式),因此它們都會因為click事件觸發一次。所以,<p>會在target階段有兩次輸出。

注意,瀏覽器總是假定click事件的目標節點,就是點選位置巢狀最深的那個節點(本例是<div>節點裡面的<p>節點)。所以,<p>節點的捕獲階段和冒泡階段,都會顯示為target階段。

事件傳播的最上層物件是window,接著依次是documenthtmldocument.documentElement)和bodydocument.body)。也就是說,上例的事件傳播順序,在捕獲階段依次為windowdocumenthtmlbodydivp,在冒泡階段依次為pdivbodyhtmldocumentwindow

說明:Event.eventPhase屬性返回一個整數常量,表示事件目前所處的階段。該屬性只讀。詳情見Event物件。

3、事件的代理

由於事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函式定義在父節點上,由父節點的監聽函式統一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。

<!DOCTYPE html>
<html>
<head>
</head>

<body>
  <ul>
    <li>點選1</li>
    <li>點選2</li>
  </ul>

<script>
  var ul = document.querySelector('ul');

  ul.addEventListener('click', function (event) {
  if (event.target.tagName.toLowerCase() === 'li') {
    // some code
     console.log(1);
  }
});
</script>
</body>
</html>

上面程式碼中,click事件的監聽函式定義在<ul>節點,但是實際上,它處理的是子節點<li>click事件。這樣做的好處是,只要定義一個監聽函式,就能處理多個子節點的事件,而不用在每個<li>節點上定義監聽函式。而且以後再新增子節點,監聽函式依然有效。

如果希望事件到某個節點為止,不再傳播,可以使用事件物件的stopPropagation方法。

<!DOCTYPE html>
<html>
<head>
</head>

<body>
	<div>
	  	<ul>
    		<li id="dj1">點選1</li>
    		<li>點選2</li>
  		</ul>
 	</div>
 	
<script>
	var li = document.querySelector('li');
 	var div = document.querySelector('div');

 	li.addEventListener('click', function (event) {
  	if (event.target.id === 'dj1') {
   	 // some code
    	 console.log(111);
  	}
	});
  	
 	li.addEventListener('click', function (event) {
  	  if (event.target.tagName.toLowerCase() === 'li') {
  	    // some code
  	     console.log(1);
  	  }
  	});
  	
  	div.addEventListener('click', function () {
	console.log(222);
	},true);//向下捕獲
	
	  // 事件傳播到 ul 元素後,就不再向下傳播了
	ul.addEventListener('click', function (event) {
	  event.stopPropagation();
	}, true);

	// 事件冒泡到 ul 元素後,就不再向上冒泡了
	ul.addEventListener('click', function (event) {
	  event.stopPropagation();
	}, false);

</script>
</body>
</html>

上面程式碼中,stopPropagation方法分別在捕獲階段和冒泡階段,阻止了事件的傳播。

但是,stopPropagation方法只會阻止事件的傳播,不會阻止該事件觸發<p>節點的其他click事件的監聽函式。也就是說,不是徹底取消click事件。

點選1時,由於該時間節點也添加了監聽函式,所以輸出結果有三個。

點選2時,由於      

 div.addEventListener('click', function () {
    console.log(222);
    },true);//向下捕獲

阻止了事件向下傳播,所以輸出結果只有一個。

如果想要徹底取消該事件,不再觸發後面所有click的監聽函式,可以使用stopImmediatePropagation方法。

p.addEventListener('click', function (event) {
  event.stopImmediatePropagation();
  console.log(1);
});

p.addEventListener('click', function(event) {
  // 不會被觸發
  console.log(2);
});

上面程式碼中,stopImmediatePropagation方法可以徹底取消這個事件,使得後面繫結的所有click監聽函式都不再觸發。所以,只會輸出1,不會輸出2。

————————————————————————————END——————————————————————————