前言
说起Javascript事件,你想到的是什么呢? 我想答案里免不了有,事件捕获,冒泡,代理之类的词在里面。
但是是否真的理解到位了呢?
javascript本身作为一门事件驱动型的语言,可见事件的重要性。
如果用户在网页上单击一个按钮,你可能想通过显示一个信息框来响应这个动作 这个就是 事件,好了稍微了解一下事件的概念,就让我们开始接下来的文章吧。
放一下文章导图:
还是一样习惯以问题开始:
- 能说说你知道的事件机制吗?
- 捕获冒泡,简单描述一下?
- 能说说事件流(事件传播)吗?
- 能说说事件代理吗?
- addEventListener(),三个参数分别是什么?第三个参数默认值,默认行为?
- 只需要一个指定的子组件捕获到事件,怎么做?如何阻止默认行为?
- JavaScript 怎么为事件绑定监听函数?
- 使用事件委托要注意什么问题?怎么解决?
- 不同子元素需要不同事件怎么处理?
- 使用removeEventListener()要注意什么呢?
希望你会,或者看完文章的你会😄
(因为下面的例子会涉及到一些监听事件已经事件对象相关的知识,所以先介绍一下)
一、事件监听
在浏览器中,事件模型就是通过监听函数对事件做出反应。事件发生后,浏览器监听到这个事件,就会执行对应的监听函数——事件驱动编程模式
在javascript中有三种方式,可以为事件绑定监听函数:
1.1 事件处理器属性
举个栗子:
栗子1:window.onload = doSomething;//注意一下这里的doSomething
栗子2:div.onclick = function(event) {
console.log('触发事件');
};
这个onload, onclick 是被用在这个情景下的事件处理器的属性,它就像其他的属性一样,但是有一个特别的地方——当您将一些代码赋值给它的时候,只要事件触发代码就会运行(像栗子2)
1.2 行内事件处理器
举个栗子:
<button onclick="doSomething()">Press me</button> //注意这里的doSomething()
"doSomething()"属性值实际上是当事件发生时要运行的JavaScript代码。也可以直接在里面写入javascript代码(但混用 HTML 和 JavaScript,文档很难解析,不好哈)。
1.3 addEventListener() 和removeEventListener()
“DOM2级事件”定义了两个方法,用于处理指定混合删除事件处理程序的操作:addEventListener(),removeEventListener(),所有的节点中都包含这两个方法,并且都有三个参数:
- 要处理的事件名
- 作为事件处理程序的函数
- 一个布尔值(true:捕获阶段调用函数;false:冒泡阶段调用函数)
- 同一个监听器addEventListener注册多个处理器(handler,handler1) 当我们使用上面的事件处理器属性方法尝试去注册多个处理器:
var btn = document.getElementById("myBtn");
var handler = function() {
console.log("我是handler()处理的");
}
var handler1 = function() {
console.log("我是handler1()处理的");
}
btn.onclick = handler;
btn.onclick = handler1;
只打印了一个结果,因为后面任何设置的属性都会尝试覆盖较早的属性,使用addEventListener
解决这个问题:
var btn = document.getElementById("myBtn");
var handler = function() {
console.log("我是handler()处理的");
}
var handler1 = function() {
console.log("我是handler1()处理的");
}
btn.addEventListener("click",handler,false);
btn.addEventListener("click",handler1,false);
- removeEventListener()的使用
通过
addEventListener()
,而removeEventListener()
则是用来移除,并且需要注意的是传入的参数与添加处理程序时使用的参数相同。
正确的:
btn.addEventListener("click",handler,false);
btn.removeEventListener("click",handler,false);
错误的:
btn.addEventListener("click",handler,false);
btn.removeEventListener("click",handler1,false);
并且匿名函数也是不行的
btn.addEventListener("click",function() {
console.log(1)
},false);
btn.removeEventListener("click",function() {
console.log(2)
},false);
二、事件对象
在触发DOM上的某个事件时,会产生一个事件对象Event,这个对象中包含着所有与事件有关的信息,包括导致事件的元素,事件的类型,以及其他与特定事件相关的信息。
并且Event对象本身就是一个构造函数,可以用来生成实例。
event = new Event(type, options);
那作为一个构造函数,就有它自己的一些属性和方法,下面围绕这个展开一下:(之后的例子会用到一些,所以这里可以先了解一下)
2.1 属性
bubbles
表明事件是否冒泡 (Event构造函数生成的事件,默认是不冒泡的)target
可以返回事件的目标节点,target就可以表示为当前的事件操作的domeventPhase
调用事件处理程序的阶段: 1——表示捕获阶段
2——表示“处于目标”
3——表示冒泡阶段
<body>
<button id="myBtn">按钮</button>
</body>
<script>
var btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.eventPhase,"btn.onclick");
}
document.body.addEventListener("click",function(event) {
console.log(event.eventPhase,"document.body.addEventListener")
},true)
document.body.onclick = function(event) {
console.log(event.eventPhase,"document.body.onclick");
}
cancelable
是否取消事件的默认行为defaultPrevented
为true表示已经调用了preventDefault()currentTarget
事件处理程序正在处理时间的那个元素target
事件的目标
有关一个this,currentTarget,target的指向问题:
在事件处理程序内部,对象this始终等于currentTarget,而target则只包含事件的实例目标,当直接指定了目标元素 -- 下面例子的btn.onclick
,则this,currentTarget,target包含相同值。
var btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.currentTarget == this); //true
console.log(event.target == this); //true
}
如果不直接指定目标元素:
event.currentTarget,this
都指向了document.body
,而event.target
却指向了document.getElementById("myBtn")
,因为它是click事件的目标。
因为按钮上没有注册事件处理程序,结果click事件冒泡到了document.body,在那里事件得到了处理。
var btn = document.getElementById("myBtn");
document.body.onclick = function(event) {
console.log(event.currentTarget === document.body);
console.log(this === document.body);
console.log(event.target === document.getElementById("myBtn"));
}
2.2 方法
Event.preventDefault()
取消事件的默认行为,配合cancelable为true使用Event.stopPropagation()
取消事件的进一步捕获或冒泡,配合bubbles为true使用Event.composedPath()
返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。Event.stopImmediatePropagation()
取消事件的进一步捕获或冒泡,同时阻止任何事件处理函数被调用。(有点像stopPropagation()的升级版)
三、事件机制
3.1 事件冒泡
事件开始时,由具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="myDiv">按钮</div>
</body>
</html>
如果单击了页面中的
graph TD
Element-div --> Element-body --> Element-html --> Document
click事件首先在div元素上发送,而这个元素就是我们单击的位置,然后click事件沿着DOM树向上(这里我图画的不是很好,本来应该向上的)传播,在每一级节点是哪个都会发生,一直到 document对象。
3.2 事件捕获
事件捕获是不太具体的节点更早接收到事件,而具体的节点应该最后接收到事件,在花时间到达预定目标之前捕获它。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="myDiv">按钮</div>
</body>
</html>
如果单击了页面中的
graph TD
Document --> Element-html --> Element-body --> Element-div
在事件捕获中,document对象先接收到click事件,然后沿着DOM树依次向下,一直到事件的实际目标。
四、事件流(事件传播)
一个事件发生后,会在父元素和子元素中进行传播,这种传播也叫事件流,分成三个阶段:
- 从window对象传导到目标节点,称为捕获阶段
- 在目标节点上触发,称为“目标阶段”
- 从目标节点传回window对象,称为“冒泡阶段”
(下图的箭头,先向下捕获再往上冒泡)
graph TD
Document --> Element-html --> Element-body --> Element-div --> Element-body --> Element-html --> Document
结合上面的属性方法和已知的冒泡捕获的概念理解,来看看下面这个例子吧:
<body>
<div>
<p>点击</p>
</div>
</body>
<script>
var phases = {
1: '捕获',
2: '目标',
3: '冒泡'
};
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', callback, true); // 捕获时调用callback
p.addEventListener('click', callback, true); // 捕获时调用 callback
div.addEventListener('click', callback, false); // 冒泡时调用 callback
p.addEventListener('click', callback, false); // 冒泡时调用 callback
function callback(event) {
var tag = event.currentTarget.tagName; // 指定目标的标签名 div p
var phase = phases[event.eventPhase]; //调用事件处理程序的阶段
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
</script>
分析一下:
- 由上图在控制台打印的结果,可以看出click事件被触发了四次:节点的捕获阶段和冒泡阶段各1次,
节点的目标阶段触发了2次。
- 三个阶段 注意:
在捕获阶段依次为window、document、html、body、div、p;
在冒泡阶段依次为p、div、body、html、document、window;
(下面我只把div和p画了图哦)
- 捕获阶段:触发了div的click事件
graph TD
Element-div --> Element-p
-
目标阶段: 事件到达p,触发了p的click事件
-
冒泡阶段:再次触发div的click事件
graph TD
Element-p --> Element-div
- 监听函数
p.addEventListener('click', callback, true);
p.addEventListener('click', callback, false);
p节点上有个监听函数,他们都会因为click事件触发,因此在目标阶段有两次输出
五、事件委托(事件代理)
在javascript中,添加到页面的时间处理程序将直接关系到页面的整体运行性能,而对于“事件处理程序过多”问题的解决方案就是——事件委托,也是我们说的事件代理。
(希望你已经看完了,或者说已经理解文章前面的知识点,这对理解事件委托机制有很大帮助)
5.1 初探
好吧,先来个例子,把不用事件委托和用事件委托来做个比较:
<ul id="link">
<li>1</li>
</ul>
实现点击li,触发事件处理程序
不用事件委托的情况:
可以直接在li标签上定义事件,但是这个操作就需要先找到ul标签,再遍历里标签(ul1.getElementsByTagName('li')得到的是数组),点击li的时候,找到目标的li的位置才可执行操作。而且每次都要重复找li标签。
var ul1 = document.getElementById('link');
var li1 = ul1.getElementsByTagName('li');
for(var i=0;i<li1.length;i++){
li1[i].addEventListener("click",function() {
console.log("在li元素上加事件处理程序")
})
}
使用事件委托:
var ul1 = document.getElementById('link');
var li1 = ul1.getElementsByTagName('li');
ul1.addEventListener("click",function() {
console.log("在ul元素上加事件处理程序")
})
这里用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发
但是这里有个小问题哈,就是点击ul也是有触发事件的,我怎么才能只点击li的时候触发,在点击ul的时候不触发呢?
5.2 完善
这个问题就要结合我们之前说的target,在下面这段代码中,我们使用了事件委托只为ul元素添加了监听事件,由于li是这个元素的子节点,而且它的事件会冒泡,所以单击事件最终会被这个函数处理。
我们知道,事件目标是被单击的列表项,event.target指向的就是点击的列表项。
var ul1 = document.getElementById('link');
// var li1 = ul1.getElementsByTagName('li');
ul1.addEventListener("click",function(event){
var event = event || window.event;
var target = event.target;
if(target.nodeName.toLowerCase() == 'li'){
console.log(123);
console.log(target.innerHTML);
}
})
5.3 升级
这个是一个列表项,如果我需要为三个li添加三个不同的事件呢?:
<ul id="myLinks">
<li id="jingda1">jingda1</li>
<li id="jingda2">jingda2</li>
<li id="jingda3">jingda3</li>
</ul>
- 不用事件委托:
<script>
var item1 = document.getElementById('jingda1')
var item2 = document.getElementById('jingda2')
var item3 = document.getElementById('jingda3')
item1.addEventListener("click",function() {
console.log(1)
})
item2.addEventListener("click",function() {
console.log(2)
})
item3.addEventListener("click",function() {
console.log(3)
})
</script>
- 使用事件委托
var ul1 = document.getElementById('myLinks');
ul1.addEventListener("click",function(event){
var event = event || window.event;
var target = event.target;
// console.log(target.id)
switch(target.id) {
case 'jingda1':
console.log("jingda1");
break;
case 'jingda2':
console.log("jingda2");
break;
case 'jingda3':
console.log("jingda3");
break;
}
事件目标是被单击的列表项,event.target指向的就是点击的列表项,通过检测target.id属性来决定采用适当的操作。
总结
有关javascript事件相关的知识就先说到这里了,有很多东西可能并没有总结到。或者说是有错的地方,希望多多建议
(还有一个大类我没有总结-javascript的事件类型,希望各位有时间也可以理一下,这篇文章我还是重点想把事件机制拉出来讲。)
关于javascript事件这个话题,可能平时用的并不是很多,但是我们必须要了解。比如在学有关react知识的时候,有关setSate是异步还是同步这个问题,中间会涉及到合成事件。而要深入理解这个知识点就必须先要理解javascript的事件机制。【这似乎又是一个小盲区,头秃】
好啦,希望看完文章的你或多或少对javascript事件又多了一些了解。可能还有很多疑问的地方,最后推荐几个我在学习和参考的相关知识点的书籍和文章。
我是婧大,一名大三学崽,希望和你一起学习一起进步。🙆🙆🙆
加油!
文章肯定有写的不好的地方,欢迎评论区指正。
写的不全的地方,也建议大家阅读一下后面的参考文献呀👇👇👇
【参考文章💬】