【Web APIs-Day3】事件进阶与页面交互
📺 对应视频:P105-P116 | 🎯 核心目标:掌握事件流机制、事件委托、滚动事件、offset/client尺寸属性
一、事件流
1.1 什么是事件流?
点击一个元素时,事件不只在目标元素上触发,而是经历一个传播过程(事件流):
事件流三阶段:
1. 捕获阶段(Capture):从 window → document → html → body → ... → 目标元素
2. 目标阶段(Target):到达目标元素
3. 冒泡阶段(Bubble):从目标元素 → ... → body → html → document → window
<div class="outer">
<div class="inner">
<button>点我</button>
</div>
</div>
// 点击 button,事件传播顺序:
// 捕获:window → document → html → body → outer → inner → button
// 冒泡:button → inner → outer → body → html → document → window
1.2 addEventListener 第三个参数
// 第三个参数:true = 捕获阶段监听,false = 冒泡阶段监听(默认)
element.addEventListener('click', handler, false) // 冒泡(默认)
element.addEventListener('click', handler, true) // 捕获
// 或传对象(更灵活)
element.addEventListener('click', handler, {
capture: false, // 是否在捕获阶段
once: true, // 只触发一次
passive: true // 不会调用 preventDefault(用于滚动优化)
})
1.3 阻止冒泡
document.querySelector('.inner').addEventListener('click', e => {
console.log('inner 被点击')
e.stopPropagation() // 阻止继续冒泡到 outer
})
document.querySelector('.outer').addEventListener('click', e => {
console.log('outer 被点击') // 不会执行,因为冒泡被阻止了
})
二、事件委托(Event Delegation)
2.1 为什么需要事件委托?
// 问题:给 100 个 li 各绑一个事件,性能差;动态新增的 li 更没有事件
const lis = document.querySelectorAll('li')
lis.forEach(li => {
li.addEventListener('click', () => { ... }) // 100 个监听器!
})
2.2 事件委托原理
利用冒泡,把事件绑定到父元素,通过 e.target 判断实际点击的子元素。
// 只绑定一个监听器在父元素 ul 上
const ul = document.querySelector('ul')
ul.addEventListener('click', e => {
// e.target:实际被点击的元素
if (e.target.tagName === 'LI') {
console.log('点击了 li:', e.target.textContent)
e.target.classList.toggle('active')
}
})
2.3 更精准的目标匹配
ul.addEventListener('click', e => {
// matches:判断元素是否匹配选择器
if (e.target.matches('li.item')) {
console.log('匹配到 .item 类的 li')
}
// closest:从当前元素向上查找最近的匹配祖先
const li = e.target.closest('li')
if (li) {
console.log('找到最近的 li 祖先:', li)
}
})
2.4 事件委托实战:动态列表
const ul = document.querySelector('.todo-list')
const input = document.querySelector('input')
const addBtn = document.querySelector('.add-btn')
// 添加新项
addBtn.addEventListener('click', () => {
if (!input.value.trim()) return
const li = document.createElement('li')
li.innerHTML = `
<span>${input.value}</span>
<button class="del-btn">删除</button>
`
ul.appendChild(li)
input.value = ''
})
// 用事件委托处理删除(包括动态新增的元素)
ul.addEventListener('click', e => {
if (e.target.classList.contains('del-btn')) {
e.target.closest('li').remove()
}
})
三、滚动事件与页面尺寸
3.1 scroll 事件
// 监听页面滚动
window.addEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop // 垂直滚动距离
const scrollLeft = document.documentElement.scrollLeft // 水平滚动距离
// 回到顶部按钮显隐
const backToTop = document.querySelector('.back-top')
backToTop.style.display = scrollTop > 300 ? 'block' : 'none'
})
// 监听某个元素内的滚动
const box = document.querySelector('.scroll-box')
box.addEventListener('scroll', () => {
console.log('盒子内滚动:', box.scrollTop)
})
// 回到顶部
document.querySelector('.back-top').addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' }) // 平滑滚动
// 或 document.documentElement.scrollTop = 0(直接跳)
})
3.2 scroll 相关属性
// 页面滚动距离(读写)
document.documentElement.scrollTop // 垂直滚动量(常用)
document.documentElement.scrollLeft // 水平滚动量
// 元素内容的完整高度(含不可见部分)
element.scrollHeight // 内容总高度
element.scrollWidth // 内容总宽度
// 判断是否滚动到底部
element.scrollTop + element.clientHeight >= element.scrollHeight
四、元素尺寸属性
4.1 offsetWidth / offsetHeight
const box = document.querySelector('.box')
// offset:元素实际占用空间(含 padding + border,不含 margin)
box.offsetWidth // 宽度(content + padding + border)
box.offsetHeight // 高度
// offsetLeft / offsetTop:相对于最近的定位祖先元素的距离
box.offsetLeft // 距定位父元素左侧的距离
box.offsetTop // 距定位父元素顶部的距离
// offsetParent:最近的定位祖先元素(position不为static)
box.offsetParent // 如果没有定位祖先,返回 body
4.2 clientWidth / clientHeight
// client:元素可见区域(含 padding,不含 border 和 margin)
box.clientWidth // content + padding
box.clientHeight
// clientLeft / clientTop:左/上边框的宽度
box.clientLeft // 左边框宽度(即 border-left-width)
box.clientTop // 上边框宽度
// 获取视口(浏览器可见区域)大小
document.documentElement.clientWidth // 视口宽度(不含滚动条)
document.documentElement.clientHeight // 视口高度
window.innerWidth // 视口宽度(含滚动条)
window.innerHeight // 视口高度
4.3 getBoundingClientRect()
// 获取元素相对于视口的位置和尺寸(最常用!)
const rect = box.getBoundingClientRect()
rect.top // 元素顶部距视口顶部的距离(随滚动变化)
rect.bottom // 元素底部距视口顶部的距离
rect.left // 元素左侧距视口左侧的距离
rect.right // 元素右侧距视口左侧的距离
rect.width // 元素宽度(与 offsetWidth 类似)
rect.height // 元素高度
// 实际应用:懒加载图片
window.addEventListener('scroll', () => {
document.querySelectorAll('img[data-src]').forEach(img => {
const rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
img.removeAttribute('data-src')
}
})
})
4.4 三者速查对比
属性 包含内容 用途
offsetWidth content + padding + border 获取元素真实占位大小
clientWidth content + padding 获取内容区大小
scrollWidth content(含隐藏部分) 判断是否有溢出内容
五、综合案例
案例:吸顶导航栏
const nav = document.querySelector('.nav')
const navTop = nav.offsetTop // 导航到顶部的距离
window.addEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop
if (scrollTop >= navTop) {
nav.classList.add('fixed') // 切换为固定定位
} else {
nav.classList.remove('fixed')
}
})
案例:无限滚动加载
window.addEventListener('scroll', () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement
// 距底部 100px 时开始加载
if (scrollTop + clientHeight >= scrollHeight - 100) {
loadMoreData()
}
})
六、知识图谱
事件进阶与页面交互
├── 事件流
│ ├── 捕获阶段(从上到下)
│ ├── 目标阶段
│ └── 冒泡阶段(从下到上)
├── 事件委托
│ ├── 原理:冒泡 + e.target 判断
│ ├── 优势:少绑定、支持动态元素
│ └── 工具:matches() / closest()
├── 滚动
│ ├── scroll 事件
│ ├── scrollTop / scrollLeft(滚动距离)
│ └── scrollTo({ behavior: 'smooth' })
└── 元素尺寸
├── offsetWidth/Height(含border)
├── clientWidth/Height(含padding)
├── scrollWidth/Height(含溢出)
└── getBoundingClientRect()(视口位置)
七、高频面试题
Q1:事件冒泡和事件捕获的区别?
捕获从 window 向下传播到目标元素;冒泡从目标元素向上传播到 window。
addEventListener默认在冒泡阶段触发(第三参数 false),传 true 则在捕获阶段触发。
Q2:什么是事件委托?有什么优势?
把子元素的事件监听绑定在父元素上,利用冒泡通过
e.target判断实际触发者。优势:① 减少监听器数量,节省内存;② 自动处理动态添加的子元素;③ 减少重复代码。
Q3: offsetWidth 和 clientWidth 的区别?
offsetWidth= content + padding + border,是元素在布局中真实占用的宽度;clientWidth= content + padding,不含 border,是元素的可视内容区域宽度。
⬅️ 上一篇:Web APIs Day2 - 事件监听 ➡️ 下一篇:Web APIs Day4 - 节点操作与移动端事件