Web APIs 学习第四天:DOM事件进阶
Web APIs 学习Day4
进一步学习事件进阶,实现更多交互的网页特效,结合事件流的特征优化事件执行的效率
- 掌握阻止事件冒泡的方法
- 理解事件委托的实现原理
文章目录
- Web APIs 学习Day4
- 事件流
- 事件捕获
- 事件冒泡
- 阻止冒泡
- 阻止默认行为
- 解绑事件
- on 事件方式(L0事件移除解绑)
- addEventListener 事件方式(L2事件解绑)
- 补充
- 补充一:鼠标经过事件的区别
- 补充二:两种注册事件的区别
- 事件委托
- 其他事件
- 页面加载事件
- 事件名:load
- 事件名:DOMContentLoaded
- 页面滚动事件
- 事件名:scroll
- 获取位置
- 页面尺寸事件
- 获取元素宽高
- 元素尺寸与位置
- 获取位置方法一:尺寸offsetWidth/offsetHeight
- 获取位置方法二:getBoundingClientRect()
- 总结
事件流
事件流是对事件执行过程的描述,是事件完整执行过程中的流动路径。事件流是事件在执行时的底层机制,主要体现在父子盒子之间事件的执行上。
了解事件的执行过程有助于加深对事件的理解,提升开发实践中对事件运用的灵活度。

如上图所示,任意事件被触发时总会经历两个阶段:捕获阶段和冒泡阶段。
简言之,捕获阶段是【从父到子】的传导过程,冒泡阶段是【从子向父】的传导过程。
事件捕获
概念:从 DOM 的根元素开始去执行对应的事件(从外到里)
事件捕获需要写对应的代码才能看到效果,代码:
DOM.addEventListener(事件类型, 事件处理函数, 是否使用捕获机制)
说明:
- addEventListener 第三个参数传入
true代表是捕获阶段触发(当然在开发中我们很少使用这个参数) - 若传入
false代表冒泡阶段,默认就是false - 若是用 L0 事件监听(
事件源.on事件类型 = function(){}),则只有冒泡阶段,没有捕获。早期 IE 也不支持捕获
了解了什么是事件捕获之后,我们来看事件捕获是如何执行的:
<body><div class="outer"><div class="inner"><div class="child"></div></div></div><script>// 获取嵌套的3个节点const outer = document.querySelector('.outer');const inner = document.querySelector('.inner');const child = document.querySelector('.child');// 外层的盒子添加事件outer.addEventListener('click', function () {console.log('我是爷爷')}, true)// 中间的盒子添加事件outer.addEventListener('click', function () {console.log('我是爸爸')}, true)// 内层的盒子添加事件outer.addEventListener('click', function () {console.log('我是儿子')}, true)</script>
</body>
执行上述代码后发现,当单击内层(儿子)盒子的事件触发时,其祖先元素的单击事件也相继触发,这是为什么呢?
结合事件流的特征,我们知道当某个元素的事件被触发时,事件总是会先经过其祖先才能到达当前元素,然后再由当前元素向祖先传递,事件在流动的过程中遇到相同的事件便会被触发。
再来关注一个细节就是事件相继触发的【执行顺序】,事件的执行顺序是可控制的,即可以在捕获阶段被执行,也可以在冒泡阶段被执行。
如果事件是在冒泡阶段执行的,我们称为冒泡模式,它会先执行子盒子事件再去执行父盒子事件,默认是冒泡模式。如果事件是在捕获阶段执行的,我们称为捕获模式,它会先执行父盒子事件再去执行子盒子事件。
事件冒泡
概念:当一个元素的事件被触发时,同样的事件将会在该元素的所有组祖先元素中依次被触发,这个过程被称为事件冒泡。
事件冒泡其实是默认的。只不过在执行事件流时,祖先元素一般情况下都没有事件,因此看上去就像只发生了当前事件一样。
L2 事件监听第三个参数是 false,默认都是冒泡。L0 也是。
我们还是通过代码来了解一下:
<body><div class="outer"><div class="inner"><div class="child"></div></div></div><script>// 获取嵌套的3个节点const outer = document.querySelector('.outer');const inner = document.querySelector('.inner');const child = document.querySelector('.child');// 外层的盒子添加事件outer.addEventListener('click', function () {console.log('我是爷爷')})// 中间的盒子添加事件outer.addEventListener('click', function () {console.log('我是爸爸')})// 内层的盒子添加事件outer.addEventListener('click', function () {console.log('我是儿子')})</script>
</body>
你会发现控制台先打印‘我是儿子’,接着才是‘爸爸’和‘爷爷’。
阻止冒泡
其实很多时候我们都是不希望出现事件冒泡的,因此我们会主动去阻止冒泡的发生。
问题:因为默认就有冒泡模式的存在,所有容易导致影响到父级元素
若想把事件就限制在当前元素内,就需要阻止事件冒泡,而阻止事件冒泡需要拿到事件对象。
语法:事件对象.stopPropagation()
注意:此方法可以阻断事件流的传播,不光是在冒泡阶段有效,捕获阶段也有效。
因此,事实上不是阻止冒泡,而是直接阻止事件流!
<body><div class="outer"><div class="inner"><div class="child"></div></div></div><script>// 获取嵌套的3个节点const outer = document.querySelector('.outer');const inner = document.querySelector('.inner');const child = document.querySelector('.child');// 外层的盒子添加事件outer.addEventListener('click', function () {console.log('我是爷爷')})// 中间的盒子添加事件outer.addEventListener('click', function () {console.log('我是爸爸')})// 内层的盒子添加事件outer.addEventListener('click', function () {console.log('我是儿子')})// 阻止事件冒泡e.stopPropagation()</script>
</body>
阻止默认行为
我们在某些情况下需要阻止默认行为的发生,比如,阻止链接的跳转、表单域跳转
语法:e.preventDefault()
举个例子,我们创建一个表单,表单里有一个提交表单的按钮,通常情况下,我们只要一点击就会提交表单,这就是表单域的默认行为。
<form action="http://www.itcast.cn"><input type="submit" value="免费注册">
</form>
运行后,我们直接点击按钮即可跳转至网页。但是在实际开发中,如果用户没有填写表单就点击提交按钮,通常页面会跳出提示并阻止表单域跳转。这是怎么做到的呢?
其实这就是阻止表单的默认行为。我们可以:
<form action="http://www.itcast.cn"><input type="submit" value="免费注册">
</form>
<script>const form = document.querySelector('form')form.addEventListener('submit', function(e){// 阻止表单默认行为 提交e.preventDefault()})
</script>
除了表单域跳转,还有链接跳转,阻止方法也是类似的。
解绑事件
有两种方式:on 事件方式和 addEventListener 事件方式
on 事件方式(L0事件移除解绑)
直接使用 null 覆盖偶就可以实现事件的解绑。
// 绑定事件
btn.onclick = function () {alert('点击了')
}
// 解绑事件
btn.onclick = null// 如果把解绑写在事件里 事件会只执行一次就再也不执行了
btns.onclick = function () {alert('你好呀')btn.onclick = null
}
addEventListener 事件方式(L2事件解绑)
如果使用 addEventListener 方式,必须使用:
removeEventListener(事件类型, 事件处理函数, [获取捕获或者冒泡阶段])
例如:
function () {alert('点击了')
}
// 绑定事件
btn.addEventListener('click', fn)
// 解绑事件
btn.removeEventListener('click', fn)
注意:匿名函数无法被解绑!
补充
补充一:鼠标经过事件的区别
鼠标经过事件其实是有两种写法,两种没有太大差别,但是
- mouseover 和 mouseout 会有冒泡效果
- mouseenter 和 mouseleave 没有冒泡效果(推荐)
补充二:两种注册事件的区别
- 传统 on 注册(L0)
- 同一个对象,后面注册的事件会覆盖前面注册的事件(这里指同一个事件)
- 直接使用 null 覆盖偶就可以实现事件的解绑
- 都是冒泡阶段执行的
- 事件监听注册(L2)
- 语法:
addEventListener(事件类型, 事件处理函数, 是否使用捕获机制) - 后面注册的事件捕获覆盖前面注册的事件(同一个事件)
- 可以通过第三个参数去确认狮子啊冒泡或者捕获阶段执行
- 必须使用
removeEventListener(事件类型, 事件处理函数, [获取捕获或者冒泡阶段])解绑 - 匿名函数无法被解绑
- 语法:
事件委托
其实事件冒泡并非只有弊端,有些时候,我们需要使用它来更方便地解决一些问题。
思考一下,如果我们同时给多个元素注册事件,比如给 ul 中的每个小 li 添加点击事件。我们通常是使用 for 循环来遍历元素注册事件。这样其实还挺麻烦的,有没有一种技巧,只注册一次就能完成以上效果呢?
答案是肯定的,这就需要回到一次我们的主题——事件委托上了。
事件委托是利用事件流的特征解决一些现实开发需求的知识技巧,主要的作用是提升程序效率。
事件委托的优点是:减少注册次数,可以提高程序性能。
原理:利用事件冒泡的特点:
- 给父元素注册事件。当我们触发子元素的时候,会冒泡到父元素身上(我们没有给子元素注册点击事件),从而触发父元素的事件

- 实现:
事件对象.target.tagName可以获得真正触发事件的元素
我们举个例子来更好地理解一下事件委托:
<ul><li>第1个孩子</li><li>第2个孩子</li><li>第3个孩子</li><li>第4个孩子</li><li>第5个孩子</li><p>我不需要变色</p>
</ul>
<script>// 点击每个小li 当前li 文字变为红色// 按照事件委托的方式 委托给父级,事件写到父级身上// 1. 获得父元素const ul = document.querySelector('ul')ul.addEventListener('click', function () {alert(11)})
</script>
利用事件流的特征,事件的的冒泡模式总是会将事件流向其父元素的,如果父元素监听了相同的事件类型,那么父元素的事件就会被触发并执行,正是利用这一特征,,我们不再需要使用复杂的 for 循环来实现效果。现在,我们只要点击任意一个小 li 就能弹出弹框。
如果,我们想实现点击某个 li,被点击的 li 就会变红色这一效果,怎么实现呢?
直接添加更改颜色的代码?
const ul = document.querySelector('ul')
ul.addEventListener('click', function () {this.style.color = 'red'
})
运行后你会发现,当你点击任意一个小 li,全部的小 li 都变红色了。这和我们想要的效果相离。
还记得之前我们学到的事件对象e吗?在e中有一个属性叫target,它记录我们鼠标点击的那个对象。比如鼠标点击 ul 里的 li,target会记录 li。我们可以在代码中尝试查看 target,发现target 里有一个属性tagName,其实,这个 tagName 才是记录当前点击的元素的真正属性。我们可以借用这个属性来设置颜色状态。
const ul = document.querySelector('ul')
ul.addEventListener('click', function (e) {if (e.target.tagName === 'LI') { // 注意记录的元素格式是大写e.target.style.color = 'red'}
})
// e.target 相当于 当前所指的元素标签
很明显,这个实现方法比我们通过 for 循环来一次修改样式要效率很多!
其他事件
页面加载事件
加载外部资源(如图片、外联CSS和JavaScript等)加载完毕时触发的事件
使用场景:
- 有些时候需要等页面资源全部处理完了做一些事情
- 老代码喜欢把 script 写在 head 中,这时候直接找 dom 元素找不到(因为代码是自上而下执行的,因此会先执行 JavaScript 中的代码,此时 html 中的元素和样式 css 还没在页面中加载,导致无法获取)
有两种加载事件语法,其事件名各不同。
事件名:load
语法:监听页面所有资源加载完毕:
- 给 window 添加 load 事件
window.addEventListener('load', function() {// 执行的操作
})
比如:我们在页面中添加一个按钮,但是把 script 写在 head 后,此时 script 中的代码需要等待 button 加载后才能执行,因此我们要添加页面加载事件。
<!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><script>// 等待页面所有资源加载完毕,就回去执行回调函数window.addEventListener('load', function () {const btn = document.querySelector('button')btn.addEventListener('click', function () {alert(11)})})</script>
</head><body><button>点击</button></body></html>
注意:页面加载事件不仅可以监听整个页面资源加载完毕,也可以针对某个资源绑定 load 事件。
比如:页面中有很大像素的图片加载的比较慢,我们针也对这个图片专门去绑定一个事件
<script>img.addEventListener('load', function () {// 等待图片加载完毕,再去执行里面的代码})
</script>
事件名:DOMContentLoaded
有时候,我们甚至可以只等到页面的 html 元素加载出来就可以做事件监听。当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像等完全加载。这样页面加载会更快一些,效率得到提高。
语法:监听页面 DOM 加载完毕:
- 给 document 添加 DOMContentLoaded 事件
document.addEventListener('DOMContentLoaded', function () {// 执行的操作
})
在上述案例中,我们可以这么做:
<!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><script>document.addEventListener('DOMContentLoaded', function () {const btn = document.querySelector('button')btn.addEventListener('click', function () {alert(11)})})</script>
</head><body><button>点击</button></body></html>
页面滚动事件
滚动条在滚动的时候持续触发的事件
使用场景:很多网页需要检测用户把页面滚动到某个区域后做一些处理,比如固定导航栏、返回顶部,比如页面滚动到某个部分,侧边栏也标记显示到该部分。
事件名:scroll
语法:监听整个页面滚动:
- 给 window 或 document 添加 scroll 事件
window.addEventListener('scroll', function() {// 执行的操作
})
- 如果想监听某个元素内部滚动直接给那个元素加即可,同上页面加载。
我们来尝试一个案例,我们想要页面滚动一段距离,比如 100px,就让某个元素显示/隐藏,可以使用 scroll 来检测滚动距离。
在此之前,我们需要了解两个属性:scrollLeft和scrollTop
功能:
- 获取被卷去的大小
- 获取元素内容往左、往上滚出去看不到的距离
- 这两个值是可读写的
- 我们更常用scrollTop

我们尽量在 scroll 事件里去获取被卷去的距离
语法:
div.addEventListener('scroll', function(){console.log(this.scrollTop)
})
获取位置
开发中,我们常检测页面滚动的距离,比如页面滚动 100px,就可以显示一个元素:
window.addEventListener('scroll', function () {// document.documentElement 是 html 元素的获取方式// 这句话一定要写在函数里面const n = document.documentElement.scrollTop// n 数字型 不带单位console.log(n)
})
由于 scrollTop 是可读写的,所以是可以赋值的,这样打开页面后滚动条会自动跳转到固定位置:
document.documentElement.scrollTop = 800
window.addEventListener('scroll', function () {// document.documentElement 是 html 元素的获取方式const n = document.documentElement.scrollTop
})
当我们想要实现返回顶部的效果,可以直接把scrollTop赋值为0即可。
页面尺寸事件
会在窗口尺寸改变的时候触发事件:
事件名:resize
window.addEventListener('resize', function() {// 执行的代码
})
如果想检测屏幕宽度:
window.addEventListener('resize', function() {let w = document.documentElement.clientWidthconsole.log(w)
})
获取元素宽高
可以通过属性:clientWidth 和 clientHeight获取元素的可见部分宽高(不包含边框、margin、滚动条等),但是包含 padding!注意,是只读属性。

元素尺寸与位置
使用场景:
- 前面案例滚动多少距离,都是我们自己提前设定好的,但在实际开发中,最好是页面滚动到某个元素,就可以做某些事
- 简单来说,就是通过 JS 的方式,得到元素在页面中的位置
- 这样我们就可以做:页面滚动到这个位置,进行某些操作的效果了
获取位置方法一:尺寸offsetWidth/offsetHeight
获取宽高:
-
获取元素的自身宽高、包含元素自身设置的宽高、padding、border
-
offsetWidth和offsetHeight(包含 border)
-
获取出来的是数值,方便计算
-
注意: 获取的是可视宽高, 如果盒子是隐藏的,获取的结果是0
获取位置:
- 获取元素距离自己定位父级元素的左、上距离
- offsetLeft和offsetTop,注意是只读属性
举个例子:我们在 div 盒子里面套一个小盒子 p,
<!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>div {position: relative;width: 200px;height: 200px;background-color: pink;margin: 100px;}p {width: 100px;height: 100px;background-color: purple;margin: 50px;}</style>
</head><body><div><p></p></div><script>const div = document.querySelector('div')const p = document.querySelector('p')console.log(div.offsetLeft)// 检测盒子的位置 最近一级带有定位的祖先元素console.log(p.offsetLeft)</script>
</body></html>
可以看到,由于 div 盒子设置了定位,因此 p 的 offsetLeft 值是相对于 div 而言的,而 div 是相对 body 而言的:

获取位置方法二:getBoundingClientRect()
获取位置:
- 通过
element.getBoundingClientRect()返回元素的大小及其相对于视口的位置
我们在一个例子里体会一下:
<!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>body {height: 2000px;}div {width: 200px;height: 200px;background-color: pink;margin: 100px;}</style>
</head><body><div></div><script>const div = document.querySelector('div')console.log(div.getBoundingClientRect())</script>
</body></html>
当我们滚动页面的时候,盒子div的位置会相对视口发生变化,因此由getBoundingClientRect()获得的信息会随着滚动而变化。
总结

