由event target引发的关于事件流的一连串思考(一)

本文深入解析DOM事件流机制,涵盖事件捕获与冒泡的区别、事件流中的target、currentTarget和this的区别,以及如何在现代浏览器中正确使用事件监听。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

**前言:**之前的上传图片用到了event.target,但是后来仔细思考了一下,自己对event.target,this,event.currentTarget的区别完全不清楚,然后发现越学越多,为了搞清楚这个问题,把DOM事件,事件流,事件捕获,事件冒泡等全部学了一遍,收获颇丰,特别总结记录下来。

事件

事件是文档或者浏览器窗口中发生的,特定的交互瞬间。

事件是用户或者浏览器自己执行的动作,比如click(用户左键单击鼠标),load(页面加载完成),JavaScript可以通过绑定触发事件来和DOM进行交互(当然也可以直接操作DOM),由于在底层JavaScript和DOM是独立的,所以多次绑定进行绑定操作会影响页面的性能(后边会提到解决方案:事件委托)。

事件流

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

以下开始以例子详细分析事件流。 HTML:

<div>
	<ul>
		<li></li>
	</ul>
</div>
复制代码

CSS:

*{
	margin: 0;
	padding: 0;
}
div{
	width: 300px;
	height: 300px;
	position: absolute;
	top: 50%;
	left: 50%;
	margin-top: -150px;
	margin-left: -150px;
	background-color: #2578b5;
	border-radius: 50%;
}
ul{
	width: 200px;
	height: 200px;
	position: absolute;
	top: 50%;
	left: 50%;
	margin-top: -100px;
	margin-left: -100px;
	background-color: #f2de76;
	border-radius: 50%;
}
li{
	list-style: none;
	width: 100px;
	height: 100px;
	position: absolute;
	top: 50%;
	left: 50%;
	margin-top: -50px;
	margin-left: -50px;
	background-color: #afc8ba;
	border-radius: 50%;
}
复制代码

效果如下:

如图所示,思考一下:如果我们点击内层圆,就仅仅点击了内层圆吗? 很明显,我们不止点击了内层圆,而且点击了中层圆,外层圆,html,body和document。这就引出了事件流的详细定义:

事件发生时会在元素节点与根节点之间按照特定的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流。

那么问题又来了,如果我们给这些元素都绑定事件,那么这些事件的执行顺序是什么?

两种事件流模型

  • 冒泡型事件流:事件的传播是从最特定的事件目标到最不特定的事件目标。即从DOM树的叶子到根。
  • 捕获型事件流:事件的传播是从最不特定的事件目标到最特定的事件目标。即从DOM树的根到叶子。

例子中的事件传播顺序:

  • 在冒泡型事件流中,是li > ul > div > body > html > document
  • 在捕获型事件流中,是document > html > body> div > ul > li

实际中的事件流并没有完全按照标准事件流实现, 所有现代浏览器都支持事件冒泡,但在具体实现中略有差别:

  • IE5.5及更早版本中事件冒泡会跳过html元素(从body直接跳到document)。
  • IE9、Firefox、Chrome、和Safari则将事件一直冒泡到window对象。
  • IE9、Firefox、Chrome、Opera、和Safari都支持事件捕获。尽管DOM标准要求事件应该从document对象开始传播,但这些浏览器都是从window对象开始捕获事件的。
  • 由于老版本浏览器不支持,很少有人使用事件捕获。建议使用事件冒泡。
实际使用中的DOM事件流

之所以会存在两种事件流,是由于微软和网景之间的竞争造成的,幸运的是,W3C决定组合使用这两种方法,并且大多数新浏览器都遵循这两种事件流方式,所以现在完整的DOM事件流分为3个阶段:

  • 事件捕获阶段
  • 目标阶段(事件在目标上发生并处理,但事件处理被认为发生在冒泡阶段)
  • 事件冒泡阶段 尽管理论上(DOM2中规定)事件捕获阶段不会涉及事件目标,但是IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两次机会在目标对象上面操作事件

并非所有的事件都会经过冒泡阶段 。所有的事件都要经过捕获阶段和处于目标阶段,但是有些事件会跳过冒泡阶段,如获得输入焦点的focus事件和失去输入焦点的blur事件。

在网上找到了一张原型图,但是出处没找到,感谢无名氏同学。

默认情况下,事件使用冒泡事件流,不使用捕获事件流。然而,在现代浏览器中(IE9+,Chrome,Firefox),你可以显式的指定使用捕获事件流,方法是在注册事件时传入useCapture参数,将这个参数设为true。 下面我们来测试绑定事件的不同方式会有什么区别。

DOM事件绑定
DOM0

通过javascript制定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理属性。 **优点:**当前所有浏览器均支持,简单且具有跨浏览器的优势。 **缺点:**一个事件处理程序只能对应一个处理函数。

下面我们用之前的同心圆例子对DOM0事件绑定进行测试: JavaScript:

var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");

div.onclick = function(){
	console.log("div");
}
ul.onclick = function(){
	console.log("ul");
}
li.onclick = function(){
	console.log("li");
}
复制代码

此时我们点击内层圆,控制台输出如下:

可以看到,输出的顺序是从内到外,所以可以得出结论,DOM0绑定事件是在冒泡阶段执行的。

删除DOM0事件处理程序,只要将对应事件属性置为null即可。如将div上绑定事件删除:div.onclick = null

另外DOM0还有个很神奇的特性,如果我们像这样绑定div的点击事件:

div.onclick = function(){
	console.log(this);
}
复制代码

那么输出的this是div,也就是执行该方法的DOM对象,但是如果定义一个函数,然后在HTML中绑定,那么输出的this是window,这是由于在HTML中绑定相当于动态绑定,所以定义函数的this永远都是window,不会随着上下文改变。

DOM1

DOM1级主要定义的是HTML和XML文档的底层结构。DOM2和DOM3级别则在这个结构的基础上引入了更多的交互能力,也支持了更高级的XML特性。为此DOM2和DOM3级分为许多模块(模块之间具有某种关联),分别描述了DOM的某个非常具体的子集。

DOM2

DOM2级事件绑定方式指定了,添加事件绑定程序和删除事件绑定程序的方法。

addEventListener(ev,fn,useCapture);
removeEventListener(ev,fn,useCapture);
复制代码

**优点:**可以在同一DOM对象绑定多个相同事件,可以控制是在捕获阶段触发还是在冒泡阶段触发。 **缺点:**IE8及以下不支持这种写法,而是使用独有的绑定多事件方法,所以需要自己写兼容模式(之后会提到)。

还是之前同心圆的例子来测试。

第三个参数为空(即默认的false)的情况: JavaScript:

var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");

div.addEventListener("click",function(){
	console.log("div");
});
ul.addEventListener("click",function(){
	console.log("ul");
});
li.addEventListener("click",function(){
	console.log("li");
});
复制代码

点击内层圆,结果如下:

第三个参数为true的情况: JavaScript:

var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");

div.addEventListener("click",function(){
	console.log("div");
},true);
ul.addEventListener("click",function(){
	console.log("ul");
},true);
li.addEventListener("click",function(){
	console.log("li");
},true);
复制代码

点击内层圆,结果如下:

可以看到,第三个参数为空(false)则在冒泡阶段触发,第三个参数为true则在捕获阶段触发。

如果我们给同一个DOM对象同时在捕获和冒泡阶段绑定同一个类型的事件,还是同心圆的例子,给div绑定DOM0的click事件,DOM2的click事件(捕获阶段触发,冒泡阶段触发),如下。 JavaScript:

var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");

div.onclick = function(){
	console.log("dom0 冒泡");
}
div.addEventListener("click",function(){
	console.log("dom2 捕获");
},true);
div.addEventListener("click",function(){
	console.log("dom2 冒泡");
},false);
复制代码

点击内层圆,输出结果如下:

可以看到,先触发捕获事件,然后触发冒泡事件,并且DOM0和DOM2的事件互不影响,谁先绑定就先执行。

点击中层圆,输出结果如下:

结果和点击内层圆一样,没毛病。

点击外层圆,输出结果如下:

很神奇,外层圆的事件顺序不是按照事件流了,这是为什么呢。其实原因在于一直没提的目标(target)阶段。我们给外层圆绑定点击事件,点击内层圆,实际上的target是内层圆,中层圆同理。但是如果我们点击外层圆,外层圆自己就是target,这时就不分事件捕获和事件冒泡了,谁先绑定谁先执行。

addEventListener和removeEventListener有几点需要注意:

  • 如果使用匿名函数的方式执行addEventListener,则无法使用removeEventListener删除该绑定事件。
  • 如果使用具名函数的方式addEventListener,则该函数内部的this指向执行该方法的DOM对象,另外匿名函数也是指向执行该方法的DOM对象。
  • 如果addEventListener和removeEventListener第三个参数不同,则不认为是同一个事件,即removeEventListener不可以删除addEventListener绑定的事件。

IE8及以下不支持标准的addEventListener和removeEventListener,而是使用了私有方法attachEvent和detachEvent。值得注意的是,这种方法的第一个参数需要加on。

attachEvent(ev,fn);
detachEvent(ev,fn);
复制代码

使用之前同心圆的例子,在IE8测试,代码如下。 JavaScript:

var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");

div.attachEvent("onclick",function(){
	console.log("div");
});
ul.attachEvent("onclick",function(){
	console.log("ul");
});
li.attachEvent("onclick",function(){
	console.log("li");
});
复制代码

结果如下:

吐槽一下,IE8及以下不支持border-radius属性,所以已经不能算是同心圆了。

由输出结果可以看出,attachEvent会在冒泡阶段触发。

attachEvent和detachEvent也有几点需要注意:

  • 使用匿名函数作为第二个参数的attachEvent是无法被删除的。
  • 无论是使用具名函数还是匿名函数作为第二个参数,函数内部的this都会指向window。

其实我本身很反感兼容低版本IE的事情,也感谢我司对前端兼容性的要求是IE9+,但是毕竟不是每个公司都像我司一样,甚至有时候都不考虑IE9了,所以还是写一下兼容写法。还是之前同心圆的例子,代码如下。 JavaScript:

var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");

function addEvent(element,type,callback){
    if(element.addEventListener){
        element.addEventListener(type,callback,false);
    }else if(element.attachEvent){
        element.attachEvent('on' + type,callback);
    }
}
addEvent(div,'click',function(){
	console.log("绑定点击事件1");
})
addEvent(div,'click',function(){
	console.log("绑定点击事件2");
})
复制代码

在Chrome上输出结果如下:

在IE8上输出结果如下:
还是有些区别的,输出顺序这个到现在我还是没想明白,很奇怪,所以如果对顺序有要求,就还是放弃IE8及以下吧。

另外就是要注意,使用addEvent这个函数的时候,匿名函数的this在不同的浏览器是有区别的,总之IE依然是个大坑。

DOM3

DOM浏览器中可能发生的事件有很多种,不同事件类型具有不同的信息,DOM3级事件规定了一下几种事件:

  • UI事件,当用户与页面上的元素交互时触发。
  • 焦点事件,当元素获得或者失去焦点时触发。
  • 鼠标事件,当用户通过鼠标在页面上执行操作时触发。
  • 滚轮事件,当使用鼠标滚轮(或类似设备)时触发。
  • 文本事件,当在文档中,输入文本时触发。
  • 键盘事件,当用户通过键盘在页面上执行操作时触发。
  • 合成事件,当为IME(Input Method Editor,输入法编辑器)输入字符时触发。
  • 变动事件,当底层Dom结构发生变化时触发。

DOM3级事件模块在DOM2级事件的基础上重新定义了这些事件,也添加了一些新事件。包括IE9在内的主流浏览器都支持DOM2级事件,IE9也支持DOM3级事件。

另外DOM3级还定义了自定义事件,自定义事件不是由DOM原生触发的,它的目的是让开发人员创建自己的事件。

事件流的target,currentTarget和this

说了这么久,终于说到了当初写这篇博客的起因了,为了弄清楚target,currentTarget和this,不断的查资料,然后发现不只是弄清楚了这三者的区别,还对整个事件流有了初步的认识,是时候重拾起只看完第七章的《JavaScript高级程序设计》恶补基础了。

target在事件流的目标阶段。currentTarget在事件流的捕获,目标及冒泡阶段。只有当事件流处在目标阶段的时候,两个的指向才是一样的, 而当处于捕获和冒泡阶段的时候,target指向被单击的对象而currentTarget指向当前事件活动的对象(注册该事件的对象)(一般为父级)。this指向永远和currentTarget指向一致(只考虑this的普通函数调用)。

我们来进行测试,还是同心圆的例子,首先只考虑W3C标准的浏览器(此处是Chrome),代码如下。 JavaScript:

var div = document.querySelector("div");
div.onclick = function(ev){
	console.log(ev.target.nodeName);
	console.log(this.nodeName);
	console.log(ev.currentTarget.nodeName);
}
复制代码

点击内层圆,输出结果如下:

点击中层圆,输出结果如下:
点击外层圆,输出结果如下:
可以得出结论,在W3C标准的浏览器上,ev.target指向的是事件流的target,而currentTarget和this的指向保持一致,指向当前事件活动的对象。

如果涉及到兼容性问题,兼容IE8,那么在IE8上会有一些问题,首先需要使用target的兼容写法,其次IE8的event对象上是没有currentTarget属性的。

因为各个浏览器的事件对象不一样, 把主要的事件对象的属性和方法列出来:

属性/方法介绍
bubble表明事件是否冒泡
cancelable表明是否可以取消冒泡
currentTarget当前时间程序正在处理的元素, 和this一样的
defaultPreventedfalse ,如果调用了preventDefualt这个就为真了
detail与事件有关的信息(滚动事件等等)
eventPhase如果值为1表示处于捕获阶段, 值为2表示处于目标阶段,值为三表示在冒泡阶段
target or srcElement事件的目标
trusted为ture是浏览器生成的,为false是开发人员创建的(DOM3)
type事件的类型
view与元素关联的window, 我们可能跨iframe
preventDefault()取消默认事件
stopPropagation()取消冒泡或者捕获
stopImmediatePropagation()(DOM3)阻止任何事件的运行

IE下的事件对象是在window下的,而标准应该作为一个参数, 传为函数第一个参数。IE的事件对象定义的属性跟标准的不同,如:

属性/方法介绍
cancelBubble默认为false, 如果为true就是取消事件冒泡
returnValue默认是true,如果为false就取消默认事件
srcElement这个指的是target, Firefox下的也是srcElement

言归正传,使用同心圆的例子,并且使用兼容写法,代码如下。 JavaScript:

var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");

function addEvent(element,type,callback){
    if(element.addEventListener){
        element.addEventListener(type,callback,false);
    }else if(element.attachEvent){
        element.attachEvent('on' + type,callback);
    }
}
addEvent(div,'click',function(ev){
	var event = ev || window.event;
	var target = event.target || event.srcElement;
	console.log(target.nodeName);
	if(event.currentTarget){
		console.log(event.currentTarget.nodeName);
	}else{
		console.log("IE8及以下不支持currentTarget");
	}
	console.log(this.nodeName);
});
复制代码

在W3C标准的浏览器上,点击内层圆,中层圆和外层圆,输出结果与之前只考虑W3C标准的浏览器的结果相同。

在IE8上,点击内层圆,输出结果如下:

点击中层圆,输出结果如下:
点击外层圆,输出结果如下:
可以看到,target是和W3C浏览器结果保持一致的,IE8及以下不支持currentTarget, 另外attachEvent的this指向window,没有nodeName这个属性,所以一直是undefined。

颜色参考: http://zhongguose.com/ http://nipponcolors.com/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值