认识事件处理
Web页面需要经常和用户之间进行交互,而交互的过程中我们可能想要捕捉这个交互的过程。比如用户点击了某个按钮、用户在输入框里面输入了某个文本、用户鼠标经过了某个位置。
浏览器需要搭建一条JavaScript代码和事件之间的桥梁。
当某个事件发生时,让JavaScript可以相应(执行某个函数),所以我们需要针对事件编写处理程序(handler)。
如何进行事件监听呢?
- 在script中直接监听 (很少使用)
- DOM属性,通过元素的on来监听事件
- 通过EventTarget中的addEventListener来监听
<body>
// 直接在html中编写JavaScript代码(了解)
// 这种方式很不常用,因为代码复杂之后阅读性不好
<button onclick="console.log('按钮1发生了点击~');">按钮1</button>
<button class="btn2">按钮2</button>
<button class="btn3">按钮3</button>
<script>
// 1.获取元素对象
var btn2El = document.querySelector(".btn2")
var btn3El = document.querySelector(".btn3")
// 2.onclick属性,这种方法的缺点在于无法执行多个函数,如果你硬要写两个函数,后面的会将前面的覆盖
function handleClick01() {
console.log("按钮2发生了点击~")
}
btn2El.onclick = handleClick01
// 3.addEventListener(推荐),下面三个函数均会执行,因此这种方式更灵活
btn3El.addEventListener("click", function() {
console.log("第一个btn3的事件监听~")
})
btn3El.addEventListener("click", function() {
console.log("第二个btn3的事件监听~")
})
btn3El.addEventListener("click", function() {
console.log("第三个btn3的事件监听~")
})
</script>
事件的冒泡和捕获
对于事件件有一个概念叫做事件流,为什么会产生事件流呢?当我们在浏览器上对着一个元素点击时,你点击的不仅仅是这个元素本身,我们的HTML元素是存在父子元素叠加层级的,假如一个span元素是放在div元素上的, div元素是放在body元素上的, body元素是放在html元素上的,那么当你点击span元素的时候,上层所有的父元素都发生了点击。下面是一个小例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box {
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 200px;
background-color: orange;
}
.box span {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>
<body>
<div class="box">
<span></span>
</div>
<script>
// 1.获取元素
var spanEl = document.querySelector("span")
var divEl = document.querySelector("div")
var bodyEl = document.body
// 默认情况下是事件冒泡
spanEl.addEventListener("click", function() {
console.log("span元素发生了点击~冒泡")
})
divEl.addEventListener("click", function() {
console.log("div元素发生了点击~冒泡")
})
bodyEl.addEventListener("click", function() {
console.log("body元素发生了点击~冒泡")
})
// 设置希望监听事件捕获的过程(第三个参数加上true)
spanEl.addEventListener("click", function() {
console.log("span元素发生了点击~捕获")
}, true)
divEl.addEventListener("click", function() {
console.log("div元素发生了点击~捕获")
}, true)
bodyEl.addEventListener("click", function() {
console.log("body元素发生了点击~捕获")
}, true)
</script>
</body>
</html>
我们会发现默认情况下事件是从由内向外依次传递的顺序,这个顺序我们称之为事件冒泡(Event Bubble),
事实上,还有另外一种监听事件流的方式就是由外向内, 这种称之为事件捕获(Event Capture) ;
为什么会产生两种不同的处理流呢?这是因为早期浏览器开发时,不管是IE还是Netscape公司都发现了这个问题;
但是他们采用了完全相反的事件流来对事件进行了传递;IE采用了事件冒泡的方式, Netscape采用了事件捕获的方式。
注意:事件的捕获和监听浏览器都会执行(按照捕获-->监听的顺序),只不过看你监听的是哪个(上面的代码就是同时监听两个过程)。
开发中通常会使用事件冒泡,所以事件捕获了解即可。
事件对象Event
一个事件发生时,就会有和这个事件相关的很多信息:比如事件的类型是什么,你点击的是哪一个元素, 点击的位置是哪里等等相关的信息;那么这些信息会被封装到一个Event对象中,这个对象由浏览器创建,称之为event对象;该对象给我们提供了想要的一些属性,以及可以通过该对象进行某些操作。
如何获取这个event对象呢?
event对象会在传入的事件处理函数回调时,被系统传入;我们可以在回调函数中拿到这个event对象;
event对象的属性有type:事件的类型;target:当前事件发生的元素;currentTarget:当前处理事件的元素;eventPhase:事件所处的阶段;
还有offsetX、 offsetY、clientX、 clientY、pageX、 pageY、screenX、 screenY,这些就基本上用不到了。
但是其中target和currentTarget两个方法一定要区分,先看下面一段代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box {
display: flex;
width: 200px;
height: 200px;
background-color: orange;
}
span {
width: 100px;
height: 100px;
background-color: #f00;
}
</style>
</head>
<body>
<div class="box">
<span class="btn">
<button>按钮</button>
</span>
</div>
<script>
var divEl = document.querySelector("div")
divEl.onclick = function(event) {
console.log(event.target)
console.log(event.currentTarget)
console.log(event.currentTarget === event.target)
}
</script>
</body>
</html>
可以看到上面的代码我们监听的就是div元素,这时候当我们点击div,第三个输出会是true,意味着这时候target和currentTarget是相等的,但是当我们点击的是span,由于存在事件冒泡,div元素同样能监听到点击,但是事件发生的地方已经不是div了而是span,这个时候二者不相等了,target属性是获取实际发生事件的元素(被点击的那个元素span),而currentTarget属性获取的是处理事件的元素(我们监听的元素div)。
event对象常见的方法:preventDefault: 取消事件的默认行为;stopPropagation:阻止事件的进一步传递(冒泡或者捕获都可以阻止)。
一般情况下也是没有必要用到这两个方法,这里具体的用法就不解释了。
补充:回调函数
什么是回调函数?
作为参数被传递给另一个函数的函数叫作回调函数。
为什么需要回调函数?
JavaScript 按从上到下的顺序运行代码。但是,在有些情况下,必须在某些情况发生之后,代码才能运行(或者说必须运行),这就不是按顺序运行了。这是异步编程。
回调函数确保:函数在某个任务完成之前不运行,在任务完成之后立即运行。它帮助我们编写异步 JavaScript 代码,避免问题和错误。
在 JavaScript 里创建回调函数的方法是将它作为参数传递给另一个函数,然后当某个任务完成之后,立即调用它。
如何创建一个回调函数?
const message = function() {
console.log("This message is shown after 3 seconds");
}
setTimeout(message, 3000);
上面函数的作用是在控制台打印一条消息(message),它在 3 秒之后显示。
message 函数是在发生某事之后(在本示例中为 3 秒之后),而不是在此之前被调用。因此,message 函数就是一个回调函数
匿名函数也可以作为回调函数
setTimeout(function() {
console.log("This message is shown after 3 seconds");
}, 3000);
// 这里的回调函数没有名称
也可以用箭头函数写回调函数
setTimeout(() => {
console.log("This message is shown after 3 seconds");
}, 3000);
事件委托
当子元素被点击时,父元素可以通过冒泡可以监听到子元素的点击;并且可以通过event.target获取到当前监听的元素;
案例:一个ul中存放多个li,点击某一个li会变成红色
方案一:监听每一个li的点击,并且做出相应
方案二: 在ul中监听点击,并且通过event.target拿到对应的li进行处理(因为这种方案并不需要遍历后给每一个li上添加事件监听,所以它更加高效)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.active {
color: red;
font-size: 20px;
background-color: orange;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
// 每一个li都监听自己的点击, 并且有自己的处理函数
var liEls = document.querySelectorAll("li")
for (var liEl of liEls) {
// 监听点击
liEl.onclick = function(event) {
event.currentTarget.classList.add("active")
}
}
</script>
</body>
</html>
上面的所示的代码是第一种解决方案,但是效率低下,没有必要为每一个li创建一个处理函数,我们可以通过事件的冒泡统一在ul中监听,以下是改进的代码(同时增加了新功能):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.active {
color: red;
font-size: 20px;
background-color: orange;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
// 统一在ul中监听
// 新需求: 点击的li变成active, 其他的取消active
var ulEl = document.querySelector("ul")
var activeLiEl = null
ulEl.onclick = function(event) {
// 1.变量记录的方式
if (activeLiEl) {
activeLiEl.classList.remove("active")
}
// 给点击的元素添加active
event.target.classList.add("active")
// 记录最新的active对应的li
activeLiEl = event.target
}
</script>
</body>
</html>
补充:classList 属性返回元素的类名,作为 DOMTokenList 对象。该属性用于在元素中添加,移除及切换 CSS 类。classList 属性是只读的,但你可以使用 add() 和 remove() 方法修改它。