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 节点,其他对象(比如windowXMLHttpRequest等)也有这个接口,它等于是整个 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——————————————————————————

猜你喜欢

转载自blog.csdn.net/sunshine102548/article/details/84133267