JavaScript:DOM事件
事件监听
什么是事件监听
事件是编程语言中的术语,它是用来描述程序的行为或状态的,一旦行为或状态发生改变,便立即调用一个函数。
例如:用户使用【鼠标点击】网页中的一个按钮、用户使用【鼠标拖拽】网页中的一张图
其中,【鼠标点击】和【鼠标拖拽】就是事件,那么这些事情发生之后,为了让JavaScript可以知道,于是就有了事件监听。
事件监听的方式
addEventListener
是 DOM 对象专门用来添加事件监听的方法,它的两个参数分别为【事件类型】和【事件回调】。
语法:
元素对象.addEventListener("事件类型", 要执行的函数)
- 元素对象:检测谁被执行了事件,只有指定的元素对象执行了指定的事件,事件监听才会执行
- 事件类型:用在网页中的操作有非常多种,事件类型用于指定只有执行了哪一个事件,事件监听才会被触发
- 要执行的函数:当事件监听检测到目标事件发生,那么就需要做出相应的反应,此处的反应,就是执行这个函数
完成事件监听分成以下步骤:
- 获取 DOM 对象
- 通过
addEventListener
方法为 DOM 对象添加事件监听 - 等待事件触发
- 事件触发后,执行相对应的函数
示例:
<div class="skyblue"></div>
<div class="pink"></div>
<div class="gold"></div>
<div class="palegreen"></div>
<script>
const gold = document.querySelector(".gold");
gold.addEventListener("click", function () {
gold.style.backgroundColor = "red";
})
</script>
在script中,这就是一个完整的事件监听过程:
- 第一步:
const gold = document.querySelector(".gold");
这个过程是在获取gold颜色对应的盒子,即获取DOM对象
- 第二步:
gold.addEventListener()
,
这一步是在事件监听,监听的对象是gold颜色的盒子
"click"
,代表点击事件,即这个事件监听的触发条件是点击,触发对象的gold颜色的盒子。
即:点击gold颜色的盒子后,执行事件监听绑定的函数。
- 第三步:
function () {gold.style.backgroundColor = "red";}
这是被绑定的函数,事件监听条件满足后,就执行该函数。函数内部的代码表示,将gold颜色的盒子颜色改为red。
以上事件监听完成的功能是:当点击gold颜色的盒子,将其变为red色。
效果展示:
在上图中,点击其它颜色盒子,颜色不会改变,只有被绑定了事件的盒子颜色才会改变。
事件类型
点击事件
click
译成中文是【点击】的意思,它的含义是监听用户鼠标的单击操作,除了【单击】还有【双击】dblclick
<script>
// 双击事件类型
btn.addEventListener('dblclick', function () {
console.log('等待事件被触发...');
// 改变 p 标签的文字颜色
const text = document.querySelector('.text')
text.style.color = 'red'
})
</script>
鼠标事件
鼠标事件是指跟鼠标操作相关的事件,如单击、双击、移动等。
mouseenter
监听鼠标是否移入 DOM 元素mouseleave
监听鼠标是否移出 DOM 元素
键盘事件
keydown
键盘按下触发时,触发事件keyup
键盘抬起触发时,触发事件
焦点事件
focus
获得焦点时,触发事件blur
失去焦点时,触发事件
文本框输入事件
input
当用户对文本框输入时,触发事件
事件对象
事件对象是事件监听函数自带的对象,其内部存储了事件触发时的相关信息。比如点击事件中,可以获取点击的位置;在键盘事件中,可以获取按下的是哪一个键位。
获取事件对象
事件对象的获取方式十分简单,事件对象就是调用的函数的第一个参数。
语法:
元素.addEventListener("事件类型", function (e){})
在上述代码中,e
就是事件对象,你可以为这个参数取任何名字,不过一般取名为event
,ev
或者e
。
观察一下这个参数传进来了什么:
依然是刚才的代码,现在已经触发了事件,在控制台中可以看到e
是一个对象,内部有非常多的属性,这些属性就是事件出发时的相关信息。
事件对象常用属性
属性 | 含义 |
---|---|
type | 触发的事件类型 |
clientX / clientY | 获取光标相对于浏览器左上角的位置 |
offsetX / offsetY | 获取光标相对于当前事件元素左上角的位置 |
key | 用户按下的键盘的值 |
示例:
事件解绑
既然可以绑定事件,也可以在不需要执行相应事件的时候解除绑定。
语法:
解绑对象.removeEventListener("事件类型",函数)
由于一个对象可以绑定多个事件类型,而一个事件也可以绑定多个函数。所以在解绑的时候,一定要指定清楚哪一个对象,哪一个事件,哪一个函数。
效果:
const gold = document.querySelector(".gold");
const skyblue = document.querySelector(".skyblue");
function fn() {
alert("正在点击gold盒子");
}
gold.addEventListener("click",fn)
skyblue.addEventListener("click", function () {
gold.removeEventListener("click", fn);
alert("gold事件已经解绑");
})
在以上代码中,先为gold
盒子绑定了一个事件,点击的时候执行fn
函数,然后为skyblue
盒子绑定事件,当skyblue
盒子被点击,就会提示"gold事件已经解绑",并且解绑gold
的事件。
效果如下:
一开始多次点击gold盒子,都会有响应,当点击完skyblue盒子后,gold的事件就被解绑了,此后再点击gold盒子,都不会发生事件监听了。
那么代码可以优化成这样吗:
const gold = document.querySelector(".gold");
const skyblue = document.querySelector(".skyblue");
gold.addEventListener("click",function fn() {
alert("正在点击gold盒子");
})
skyblue.addEventListener("click", function () {
gold.removeEventListener("click", fn);
alert("gold事件已经解绑");
})
在gold
盒子的事件监听中,定义了fn
函数,那么fn
函数的作用域就在addEventListener
内部,此时skyblue
的事件监听是无法找到fn
函数的,系统就会报错:fn is undefined;即没有定义fn
函数。所以在确定了要依靠其它函数来解绑事件,那么就要把事件监听的函数写在外面。
此外,调用函数为匿名函数的事件监听无法解绑,因为在解绑的时候,无法找到对应函数名。
环境对象 this
this
对象是函数内部的特殊对象,其代表着函数运行时所处的环境。
当不同对象调用同一个函数,此函数的this
对象也会不同。
当直接调用某一个函数,其this
是Windows
,即JavaScript中的顶级对象。
示例:
function fn() {
console.log(this);
}
fn();
gold.addEventListener("click", fn)
skyblue.addEventListener("click", fn)
fn
是一个函数,其执行后,会向控制台输出当前的this
对象,代码中共有三处调用了fn
,看一下三个this
的输出结果:
可以看到,不同地方调用同一个函数,其this
也会不同。
this
对象的指向准则:谁调用这个函数,这个函数的this就是谁。
在实际应用中,可以用this
代指一些复杂的对象名,比如以下代码:
const aVeryVeryVeryVeryVeryVeryVeryLongName = document.querySelector(".gold");
aVeryVeryVeryVeryVeryVeryVeryLongName.addEventListener("click", function () {
aVeryVeryVeryVeryVeryVeryVeryLongName.innerHTML = "xxx";
aVeryVeryVeryVeryVeryVeryVeryLongName.style.backgroundColor = "red";
aVeryVeryVeryVeryVeryVeryVeryLongName.style.display = "blocck";
})
如果事件对象名字非常长,比如此处的aVeryVeryVeryVeryVeryVeryVeryLongName
,那么后续想要调用这个对象就会很麻烦,而在事件监听函数中,this
对象就是aVeryVeryVeryVeryVeryVeryVeryLongName
,所以代码可以简化为:
const aVeryVeryVeryVeryVeryVeryVeryLongName = document.querySelector(".gold");
aVeryVeryVeryVeryVeryVeryVeryLongName.addEventListener("click", function () {
this.innerHTML = "xxx";
this.style.backgroundColor = "red";
this.style.display = "blocck";
})
事件流
事件流是对事件执行过程的描述,事件流主要分为两个过程:事件捕获与事件冒泡
事件捕获
所谓事件捕获,就是在DOM树中找到目标事件的过程。
比如一个外国人想买到正宗的中国陶瓷,那么他就需要“捕获”到陶瓷。这个过程中,需要先在中国寻找江西省,在江西省寻找景德镇,在景德镇寻找陶瓷店铺,最后才能找到陶瓷。这个过程叫做捕获,也就是一层一层往下寻找,直到找到目标为止。
事件冒泡
当寻找到目标后,那么就需要将目标带回,此时陶瓷就会离开陶瓷店,离开景德镇,离开江西省,离开中国,最后才能到达老外手里。这个过程叫做冒泡,即事件捕获后,往外一层一层冒出。
那么再解析一下上图:
当要寻找一个div
,那么就要先事件捕获:进入document
,进入html
,进入body
,找到div
。然后再事件冒泡:离开body
,离开html
,离开document
。
事件捕获与事件冒泡的影响
事件捕获和事件冒泡在事件监听
中发挥了重要作用。
现在有以下布局的盒子:
对其绑定以下事件:
const skyblue = document.querySelector(".skyblue");
const pink = document.querySelector(".pink");
const gold = document.querySelector(".gold");
const palegreen = document.querySelector(".palegreen");
skyblue.addEventListener("click", function () {
console.log("skyblue");
})
pink.addEventListener("click", function () {
console.log("pink");
})
gold.addEventListener("click", function () {
console.log("gold");
})
palegreen.addEventListener("click", function () {
console.log("palegreen");
})
即对每个盒子都绑定了点击事件,点击后会输出当前盒子的名字,接下来点击绿色盒子试试:
此处只点击了一个盒子,四个盒子相应的事件全部触发了!
这就是事件冒泡的影响,观察一下输出顺序:绿,黄,粉,蓝,刚好就是事件冒泡的顺序。
当一个元素的事件触发时,所有祖先元素绑定的相同类型事件监听都会被触发
这个地方,四个元素全部绑定了click
的事件监听,当绿色盒子的click
事件监听触发时,其所有父级元素的click
事件监听都会被触发。
而默认的事件监听执行顺序,是在冒泡阶段执行的,这涉及到了addEventListener
的第三个参数:
- 当第三个参数为默认值
false
时,addEventListener
的函数效果会在冒泡阶段执行 - 当第三个参数为
true
时,addEventListener
的函数效果会在捕获阶段执行
现在将所有函数的第三个参数加上true
:
const skyblue = document.querySelector(".skyblue");
const pink = document.querySelector(".pink");
const gold = document.querySelector(".gold");
const palegreen = document.querySelector(".palegreen");
skyblue.addEventListener("click", function () {
console.log("skyblue");
},true)
pink.addEventListener("click", function () {
console.log("pink");
},true)
gold.addEventListener("click", function () {
console.log("gold");
},true)
palegreen.addEventListener("click", function () {
console.log("palegreen");
},true)
点击绿色盒子:
最后函数执行顺序:蓝,粉,黄,绿,即事件捕获的顺序。
事件监听和事件冒泡有利有弊,好处是可以进行事件委托
。
如果不想要其带来的影响,由于addEventListener
默认在事件冒泡阶段执行,那么就需要阻止事件冒泡的发生,称为阻止冒泡
。
阻止冒泡
语法:
事件对象.stopPropagation()
此处的事件对象,就是之前讲解的e
,即:e.stopPropagation()
,这个函数可以阻断事件流动传播,对事件冒泡和事件捕获都有效果。
将阻止冒泡加到最里层的绿色盒子中:
const skyblue = document.querySelector(".skyblue");
const pink = document.querySelector(".pink");
const gold = document.querySelector(".gold");
const palegreen = document.querySelector(".palegreen");
skyblue.addEventListener("click", function () {
console.log("skyblue");
})
pink.addEventListener("click", function () {
console.log("pink");
})
gold.addEventListener("click", function () {
console.log("gold");
})
palegreen.addEventListener("click", function (e) {
console.log("palegreen");
e.stopPropagation(); // 阻止冒泡
})
效果如下:
最后只有绿色盒子执行了事件监听,其它盒子由于事件冒泡被阻断了,没有接收到绿色盒子的冒泡。当时间冒泡触发时,被绿色盒子的stopPropagation
截断了,就不再往上冒泡,触发祖先的函数了。
事件委托
现在有一个父级盒子ul
,三个子级盒子li
:
要让三个子级盒子被点击后,都会变成白色,应该怎么做?
基础思路就是,对三个盒子分别”click“进行事件监听,然后将它们的颜色改变为白色。
但是如果你有10个子盒子,20个子盒子,一个一个去监听不仅会降低浏览器的效率,而且代码会很冗余,此时事件委托
就发挥作用。
事件委托利用了事件冒泡的特性,当点击三个子级的盒子,父级也会接收到click
事件的效果。那么此时只要监听父级盒子,当子级盒子被点击后,click
冒泡到父级盒子,父级盒子的事件监听就触发了。
那么当父级盒子事件监听触发后,要如何让子级元素产生效果?
在事件对象e
中,有一个属性值arget
,它存储着此次事件触发的元素。
示例:
const skyblue = document.querySelector(".skyblue");
const pink = document.querySelector(".pink");
const gold = document.querySelector(".gold");
const palegreen = document.querySelector(".palegreen");
skyblue.addEventListener("click", function (e) {
console.log(e.target);
})
该代码每次点击时,输出当前的e.target
:
左下角输出控制台中,点击哪一个盒子,e.target
就是谁。
此外e.target.tagName
存储着触发元素的元素名,不过是大写的:
由于我父子级元素的标签类型不同,可以使用这种方式来区分点击了子级盒子还是父级盒子:
skyblue.addEventListener("click", function (e) {
if (e.target.tagName === "LI")
{
e.target.style.backgroundColor = "#ffffff";
}
})
这样只有点击的盒子是LI
时,才能触发变白的效果:
点击小盒子的时候,会变为白色,点击大盒子则不会。
这就是事件委托:将多个子级的事件委托给父级,利用冒泡特性让父级触发效果,再用e.target
找到触发对象,来执行效果。事件委托可以有效减少代码量,和事件监听的数目。