javascript事件流、事件处理程序、事件类型

441 阅读12分钟

我们想一个问题,用户是怎么和网页交互的?静态页面中,展示很多,但交互基本是没有的,只是一些类似动态的交互,比如你点击一个超链接实现页面跳转。真正让用户和网页交互的是javascript的出现,那么javascript又是怎么支持交互的呢?

事件,用户的需求用javascript来体现就是一个个事件,那这一个过程是怎么样的呢?首先,用户需要给DOM元素绑定一个事件,在合适的时机用户触发该事件,紧接着javascript响应事件。

那么这里会涉及几个问题,怎么样绑定事件?用户触发了该事件之后,页面是怎么知道该事件已经被触发了并作出响应?

一、事件流、捕获、冒泡

1.事件流

描述的是页面中接收事件的顺序

为什么会出现事件流的说法,这里就要学习一下历史。

我们知道一个页面往往包含三个部分:HTML,CSS,JavaScript,浏览器通过解析这些部分的代码,最终以视图的形式渲染出来显示给用户。前面讲了,用户要与页面进行交互就要先绑定事件然后去触发事件,绑定是针对某一个DOM节点的,我们认为触发的时候去操作DOM节点就好,点击也好,鼠标移入也好等等。但是实际上,当你在页面上的一个区域操作时,你真正感兴趣的是哪个元素,这一点浏览器并不知道。举个例子,你把手指放在一组同心圆的圆心上,那么你的手指指向的不是一个圆,而是所有的圆。这就类似,我们给一个按钮绑定了一个点击事件,我们当然希望点击该按钮时就触发该事件,但是当你单击一个按钮时,你实际上同时也在点击按钮所在的父节点区域,body元素的区域以及html元素的区域,它们就如同一个同心圆似的。如果这些区域所在节点都绑定了点击事件,浏览器响应的顺序应该是怎么样的呢?

伴随着这个问题,两种主流的浏览器Netscape和IE有不同的解决方案。

2.事件捕获

Netscape定义了一种叫做事件捕获的处理方法,事件首先发生在DOM树的最高层对象(document)然后往最深层的元素传播。在上面的例子中,事件捕获首先发生在document上,然后是html元素,body元素,button父节点、最后是button元素。

3.事件冒泡

IE的处理方法正好相反。他们定义了一种叫事件冒泡的方法。事件冒泡认为事件触发的最深层元素首先接收事件。然后是它的父元素,依次向上,知道document对象最终接收到事件。尽管相对于html元素来说,document没有独立的视觉表现,他仍然是html元素的父元素并且事件能冒泡到document元素。所以上面例子中,button元素先接收事件,然后是button的父节点、body、html最后是document。

从上面可以知道:

Netscape Communicator 的事件流是事件捕获流

IE 的事件流是事件冒泡流

那么问题来了

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>test</title>
		<style type="text/css">
			.box1{
				width: 400px;
				height: 400px;
				background-color: yellow;
			}
			.box2{
				width: 300px;
				height: 300px;
				background-color: aquamarine;
			}
			.box3{
				width: 100px;
				height: 100px;
				background-color: blueviolet;
			}

		</style>
	</head>
	<body>
		<div class="box1">
			<div class="box2">
				<div class="box3">box3</div>
			</div>
		</div>
	</body>
	<script>
		var box1=document.querySelector(".box1");
		var box2=document.querySelector(".box2");
		var box3=document.querySelector(".box3");	
		box1.onclick=function(){
			console.log("this is box1");
		}
		box2.onclick=function(){
			console.log("this is box2");
		}
		box3.onclick=function(){
			console.log("this is box3");
		}
	</script>
</html>

假设我们回到远古时代,那时候,有一个网景浏览器,另一个是IE浏览器(假设IE6)

上面这段代码在网景浏览器运行是这样的

当我点击box3盒子时,控制台打印的是

this.box1
this.box2
this.box3

当我点击box1盒子时,控制台打印的结果是

this is box1

在IE6浏览器中运行是这样的

当我点击box3盒子时,控制台打印的结果是

this is box3
this is box2
this is box1

当我点击box1盒子时,控制台打印的结果是

this is box1

那么现在的标准是什么?W3C规定,JS支持冒泡流和捕获流,但是IE8(含IE8)之前的版本只支持冒泡流。

DOM2级事件规定的事件流包括三个阶段:

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

首先发生的是事件捕获,为截获事件提供了机会;然后是实际的目标接收到事件;最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

什么意思?新的标准告诉我们:

在用JS绑定事件时,可以设置事件流的模式,捕获和冒泡二选一,可以明确该事件是在捕获阶段还是冒泡阶段被触发;

只要是在符合W3C标准的浏览器中执行你写的JS代码,就能得到你预期的效果,这里要注意的是不符合W3C标准的浏览器是指IE9以下版本。

了解事件流有什么用呢?我们在绑定事件时需要设置事件流的模式,下面我们就看看是怎么绑定事件的,怎么设置当前事件是在捕获阶段触发还是冒泡阶段被触发。

二、事件处理程序

首先了解一下什么是事件

事件就是用户或者浏览器自身执行的某种动作。诸如click,load,mouseover都是事件的名字。而响应某个事件的函数就是事件处理程序(事件侦听器)。比如用户点击行为,在代码中如果要响应这个事件去做一些业务逻辑就必须写一个事件侦听器。

1、HTML事件处理程序

某个元素支持的每种事件都可以使用一个与之对应事件处理程序同名的HTML特性来指定。这个特性的值是可以执行的javascript代码。一般来说,我们会定义一个函数,在函数体里面定义响应该事件的逻辑。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
	<title>test</title>
    </head>
    <body>
        <button type="button" onclick="sayhi(this)">点击我试试</button>
    </body>
    <script>
        function sayhi(_this){
            console.log(event);
            console.log(_this.innerHTML);
        }
    </script>
</html>

通过event变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。在这个函数内部可以接受this从而获取该元素节点。

HTML中指定事件处理程序有两个缺点:

  • 时差问题 用户在HTML元素已出现在页面上就触发相应的事件,但是当时的事件处理程序有可能尚不具备执行条件。比如页面渲染了,但是javascript代码尚未加载,引发报错。
  • 扩展事件处理程序的作用链在不同的浏览器中会导致不同的结果 不同javascript引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。
  • HTML和JavaScript代码耦合性高,不利于维护

2、DOM0级事件处理程序

通过JavaScript指定事件处理程序就是将一个函数赋值给一个事件处理程序属性。要使用JavaScript指定事件处理程序,首先必须取得一个要操作的对象的引用。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>test</title>
	</head>
	<body>
		<button type="button" class="btn">点击我试试</button>
	</body>
	<script>
		var btn=document.querySelector(".btn");
		btn.onclick=function(){
			console.log(this.innerHTML);
		}
	</script>
</html>

也可以删除通过DOM0级方法指定的事件处理程序

btn.onclick = null;

注意:

上面两种绑定事件的方法并不能设置事件流的模式,那么默认的是事件冒泡,那我如果想设置事件流模式为事件捕获怎么办?你要采用下面这种写法

3、DOM2级事件处理程序

DOM2级事件定义了两个方法,用于处理指定和删除事件处理程序的操作:

  • addEventListener(event, function, useCapture)

  • removeEventListener(event, function, useCapture)

所有的DOM节点中都包含这两个方法,并且接收三个参数:

event : (必需)事件名,支持所有DOM事件 function:(必需)指定要事件触发时执行的函数 useCapture:(可选)指定事件是否在捕获或冒泡阶段执行。true:捕获;false:冒泡;默认false

<script>
    var btn=document.querySelector(".btn");
    btn.addEventListener("click",sayhi,false);
    function sayhi(){
    	console.log("hi");
    }
</script>

使用DOM2级方法添加事件处理程序的主要好处就是可以添加多个事件处理程序,这两个事件处理程序会按照添加它们的顺序触发。

<script>
    var btn=document.querySelector(".btn");
    btn.addEventListener("click",sayhi,false);
    btn.addEventListener("click",sayHello,false);
    function sayhi(){
    	console.log("hi");
    };
    function sayHello(){
    	console.log("hello");
    };
</script>

通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除。

需要注意的是:

  • 移除的时候传入的参数需要与添加时传入的参数相同。因此添加时不建议第二个参数使用匿名函数。

  • 多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度的兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。否则不建议在实践中捕获阶段注册事件处理程序。

  • IE9以下浏览器不支持DOM2级事件处理程序

那在IE9以下事件处理程序是怎么写的?

4、IE事件处理程序

IE提供两个方法,用于处理指定和删除事件处理程序的操作:

  • attachEvent(event, function)
  • detachEvent(event, function)

event:(必需)事件类型。注意需加“on“,例如:onclick。 function:(必需)指定要事件触发时执行的函数。

element.attachEvent(event, function)

event:(必需)事件类型。需加“on“,例如:onclick。

function:(必需)指定要事件触发时执行的函数。

<script>
    var btn=document.querySelector(".btn");
    btn.attachEvent("onclick",sayhi);
    function sayhi(){
    	console.log("hi");
    }
</script> 

attachEvent() 添加事件处理程序同样可以添加多个事件处理程序,但是多个事件处理程序的执行顺序是与添加它们的顺序相反的。

移除的时候传入的参数需要与添加时传入的参数要相同。

随着微软最新的Microsoft Edge浏览器发布,采用的内核是和谷歌一样的。因此,相信在未来我这里讨论的IE事件处理程序都是没有意义的。

不知道大家有没有发现,如果要设置事件流的模式只能在DOM2级事件处理程序中才可以。

下面来验证一下设置事件捕获实际效果如何

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>test</title>
		<style type="text/css">
			.box1{
				width: 400px;
				height: 400px;
				background-color: yellow;
			}
			.box2{
				width: 300px;
				height: 300px;
				background-color: aquamarine;
			}
			.box3{
				width: 100px;
				height: 100px;
				background-color: blueviolet;
			}
		</style>
	</head>
	<body>
		<div class="box1">
			<div class="box2">
				<div class="box3">box3</div>
			</div>
		</div>
	</body>
	<script>
		var box1=document.querySelector(".box1");
		var box2=document.querySelector(".box2");
		var box3=document.querySelector(".box3");	
		
		box1.addEventListener("click",handlerBox1,true);
		box2.addEventListener("click",handlerBox2,true);
		box3.addEventListener("click",handlerBox3,true);

		function handlerBox1(){
			console.log("this is box1");
		}
		function handlerBox2(){
			console.log("this is box2");
		}
		function handlerBox3(){
			console.log("this is box3");
		}
	</script>
</html>

当点击box3时,控制台打印的结果是

this is box1
this is box2
this is box3

修改代码

box1.addEventListener("click",handlerBox1,true);
box2.addEventListener("click",handlerBox2,false);
box3.addEventListener("click",handlerBox3,true);

点击box3时,控制台打印的结果是

this is box1
this is box3
this is box2

这里看到box2绑定的监听函数在冒泡阶段被执行

5、跨浏览器事件处理程序

下面是跨浏览器事件处理程序(兼容性写法,主要添加能力检查)

var EventUtil = {
  // element是当前元素,可以通过getElementById(id)获取
  // type 是事件类型,一般是click ,也有可能是鼠标、焦点、滚轮事件等等
  // handle 事件处理函数
  addHandler: (element, type, handler) => {
    // 先检测是否存在DOM2级方法,再检测IE的方法,最后是DOM0级方法(一般不会到这)
    if (element.addEventListener) {
      // 第三个参数false表示冒泡阶段
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent(`on${type}`, handler)
    } else {
      element[`on${type}`] = handler;
    }
  },

  removeHandler: (element, type, handler) => {
    if (element.removeEventListener) {
      // 第三个参数false表示冒泡阶段
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent(`on${type}`, handler)
    } else {
      element[`on${type}`] = null;
    }
  }
}

// 获取元素
var btn = document.getElementById('btn');
// 定义handler
var handler = function(e) {
  console.log('我被点击了');
}
// 监听事件
EventUtil.addHandler(btn, 'click', handler);
// 移除事件监听
// EventUtil.removeHandler(button1, 'click', clickEvent);

三、事件对象

在触发DOM上的某个事件时,会产生一个事件对象event。这个对象中包含着所有与事件有关的信息。包括导致事件的元素,事件的类型以及其他与特定事件相关的信息。

DOM2级事件对象

属性方法类型读写说明
bubblesBlooean只读表明事件是否冒泡
stopPropagation()Function只读取消事件的进一步捕获或冒泡,如果bubbels为true,则可以使用这个方法
cancelableBlooean只读表明是否可以取消事件的默认行为
preventDefault()Function只读取消事件的默认行为,如果cancelable为true,则可以使用这个方法
currentTargetElement只读其事件处理程序当前正在处理事件的那个元素
targetElement只读事件的目标
detailIntegar只读与事件相关的细节信息
eventPhaseIntegar只读调用事件处理程序的阶段:1表示捕获阶段2表示处于目标3表示冒泡阶段
trustedBlooean只读为true表示事件是浏览器生成的,为false表示事件是由开发人员通过js创建的
typeString只读被触发的事件的类型
viewAbstractView只读与事件关联的抽象视图。等同于发生事件的window对象

IE事件对象

属性方法类型读写说明
cancelBubbleBlooean读写默认值为false,但将其设置为true就可以取消事件冒泡,与DOM中stopPropagation()的方法作用相同
returnvalueBlooean读写默认值为true,但将其设置为fasle,就可以取消事件的默认行为,与DOM中的preventDefault()方法的作用相同
srcElementElement只读事件的目标,与DOM中的target属性相同
typeString只读被触发的事件类型

跨浏览器事件对象

var EventUtil = {
  addHandler: (element, type, handler) => {},

  removeHandler: (element, type, handler) => {},
  // 获取event对象
  getEvent: (event) => {
    return event ? event : window.event
  },
  // 获取当前目标
  getTarget: (event) => {
    return event.target ? event.target : event.srcElement
  },
  // 阻止默认行为
  preventDefault: (event) => {
    if (event.preventDefault) {
      event.preventDefault()
    } else {
      event.returnValue = false
    }
  },
  // 停止传播事件
  stopPropagation: (event) => {
    if (event,stopPropagation) {
      event.stopPropagation()
    } else {
      event.cancelBubble = true
    }
  }
}

四、事件委托

事件委托就是将原本绑定在自己身上的事件绑定到父级元素上,利用事件冒泡的原理,来触发该事件

比如下面例子

<ul id="list">
  <li id="item1" >item1</li>
  <li id="item2" >item2</li>
  <li id="item3" >item3</li>
</ul>
<script>
    var item1 = document.getElementById("item1");
    var item2 = document.getElementById("item2");
    var item3 = document.getElementById("item3");
    item1.onclick = function(){
        alert("hello item1");
    }
    item2.onclick = function(){
        alert("hello item2");
    }
    item3.onclick = function(){
        alert("hello item3");
    }
</script>

使用事件委托,只在内存中开辟了一块空间,节省资源同时减少了dom操作,提高性能

<ul id="list">
  <li id="item1" >item1</li>
  <li id="item2" >item2</li>
  <li id="item3" >item3</li>
</ul>
<script>
    var item1 = document.getElementById("item1");
    var item2 = document.getElementById("item2");
    var item3 = document.getElementById("item3");
    document.addEventListener("click",function(event){
        var target = event.target;
        if(target == item1){
            alert("hello item1");
        }else if(target == item2){
            alert("hello item2");
        }else if(target == item3){
            alert("hello item3");
        }
})
</script>

使用事件委托,对于新添加的元素也会有之前的事件

<ul id="list">
  <li id="item1" >item1</li>
  <li id="item2" >item2</li>
  <li id="item3" >item3</li>
</ul>
<script>
var list = document.getElementById("list");
document.addEventListener("click",function(event){
	var target = event.target;
	if(target.nodeName == "LI"){
		alert(target.innerHTML);
	}
})
var node=document.createElement("li");
var textnode=document.createTextNode("item4");
node.appendChild(textnode);
list.appendChild(node);
</script>

参考资料

实例分析JavaScript中的事件委托和事件绑定

JS事件:捕获与冒泡、事件处理程序、事件对象、跨浏览器、事件委托

JS 事件与事件对象小结