主题
2-8综合案例-tab栏切换案例
本页对应 第二章WebAPIs,共 5 段视频,约 43 分钟。
学习目标
- 能说明排他思想的适用场景,并解释“先排除其他元素、再保留当前元素”的执行顺序及顺序颠倒后失效的原因。
- 能使用
querySelectorAll获取一组元素,通过for循环批量绑定事件,并在回调函数中正确使用this指向触发事件的当前元素。 - 能根据 tab 栏上下结构说明切换原理:上方标题与下方内容一一对应,通过显示和隐藏对应内容实现切换。
- 能实现 tab 栏鼠标经过高亮:移除其他链接的
active类,并为当前链接添加active类,通过页面效果和控制台验证结果。 - 能实现 tab 栏底部内容跟随:先移除内容区已有
active类,再根据当前链接索引为对应item添加active类。 - 能实现轮播图点击小圆点切换图片:批量绑定点击事件,将当前小圆点索引赋给信号量
i,调用公共切换函数并验证其与左右按钮切换的联动效果。
知识点
事件解绑要解决什么问题
“移除事件处理函数”也叫“解绑事件”。它的核心不是把按钮删掉,而是把按钮上已经绑定的点击处理函数拿掉,让后续再点击时不再执行原来的逻辑。
为什么需要:有些按钮只允许点击一次,比如提交后防止重复提交、弹窗只弹一次、引导提示只出现一次;如果不解绑,用户每点一次都会重复执行。怎么想:绑定和解绑要成对出现——用什么方式绑定,就用对应方式解绑。怎么做:本节笔记用两个按钮分别演示两种方式:addEventListener 绑定的用 removeEventListener 移除;onclick 属性绑定的把 onclick 设为 null。易错点:不要以为“点过一次就自动没了”,浏览器不会自动解绑,必须在处理函数里主动移除。
用 addEventListener 绑定,就用 removeEventListener 解绑
笔记中的 L2 按钮使用事件监听方式:
html
<body>
<button class="l2">移除L2事件监听</button>
<button class="l0">移除L0事件监听</button>
<script>
// 需求:按钮就点击一次,然后移除点击事件
// 1. l2事件监听
const l2 = document.querySelector('.l2')
l2.addEventListener('click', fn)
function fn() {
alert('我点击了')
// 移除事件监听
l2.removeEventListener('click', fn)
}
// 2. l0事件监听
const l0 = document.querySelector('.l0')
l0.onclick = function () {
alert('我点击了')
// 移除事件监听
l0.onclick = null
}
</script>
</body>为什么需要:addEventListener 可以给同一元素同一事件绑定多个处理函数,所以解绑时必须精确告诉浏览器“移除哪一个函数”。怎么想:浏览器内部记录的是函数引用,不是函数体文本;因此移除时传入的函数必须和绑定时的函数是同一个。怎么做:先把处理函数写成具名函数 fn,绑定为 l2.addEventListener('click', fn),在 fn 内部执行 l2.removeEventListener('click', fn)。第一次点击时弹出提示,然后移除;第二次点击就不会再触发。易错点:如果写成 l2.addEventListener('click', function () { ... }),这个匿名函数没有名字,后面无法再拿到同一个引用,通常就不能准确移除;也不要写成 removeEventListener('click', fn()),fn() 是立即调用函数,不是传函数引用。
用 onclick 属性绑定,就用 onclick = null 解绑
L0 按钮使用 onclick 属性方式绑定:
js
const l0 = document.querySelector('.l0')
l0.onclick = function () {
alert('我点击了')
// 移除事件监听
l0.onclick = null
}为什么需要:属性式绑定简单直观,适合只挂一个处理函数的场景。怎么想:onclick 本质上就是元素对象上的一个属性,给它赋函数就是绑定,给它赋 null 就是清空。怎么做:在点击处理函数中执行 l0.onclick = null,这样属性上的处理函数被清掉,后续点击不再执行。易错点:onclick = null 只清除通过 onclick 属性绑定的处理函数,不会清除 addEventListener 注册的监听;反过来,removeEventListener 也不能移除 onclick 属性上的函数。两种方式要对应使用。
调试时重点看“函数引用”和“绑定方式”
为什么需要:事件解绑代码很短,但错误往往很隐蔽,表现是“第一次能点,第二次还能点”或“完全移除不掉”。怎么想:先确认绑定方式,再看解绑时传入的是不是同一个函数引用。怎么做:可以在点击处理函数里加 console.log 或断点,确认解绑代码确实执行;再检查事件类型是否一致,例如绑定 'click',移除也应是 'click'。易错点:事件类型字符串写错、函数引用不同、把属性式绑定和监听式解绑混用,都会导致移除失败。转写稿中有些同音字会影响理解,本节以笔记代码为准:addEventListener 是添加监听,removeEventListener 是移除监听,onclick = null 是清空属性式点击处理函数。
小结
本节要掌握两句话:addEventListener 绑定的事件,用 removeEventListener 移除,并且必须传同一个具名函数;onclick 绑定的事件,用 onclick = null 移除。学习时先运行笔记代码,分别点击 L2 和 L0,观察第一次弹窗、第二次不再弹窗的过程,就能理解“事件解绑”的实际作用。
先分清:这三类事件分别在什么时候用
本节笔记的主题是“其他事件”,包括页面加载事件、元素滚动事件和页面尺寸事件。它们共同点不是“点击某个按钮”,而是页面或窗口在某个时机、某种状态下触发。学习时要先问:我到底要等什么?是等资源加载完,还是等 DOM 解析完?是监听滚动,还是监听窗口尺寸变化?想清楚触发时机,再决定绑在 window 还是 document 上。
页面加载事件 load:等外部资源全部处理完
为什么需要:有些代码必须等页面资源全部处理完再执行;老代码喜欢把 script 写在 head 中,这时直接查找 DOM 元素可能找不到。load 就是等图片、外联 CSS、JavaScript 等外部资源加载完毕时触发。
怎么想:如果业务依赖图片尺寸、外联资源状态,或者初始化代码放在 head 里,就要考虑等 load 后再执行。
怎么做:监听页面所有资源加载完毕:
javascript
window.addEventListener('load', function() {
// xxxxx
})易错点:load 绑在 window 上,不是随便绑在某个普通元素上;它等待的是“所有资源”,所以可能比 DOM 解析完成更晚。不要把“DOM 结构可用”和“资源全部加载完”混为一谈。
DOMContentLoaded:DOM 解析完就执行
为什么需要:很多时候只需要 DOM 结构准备好,并不需要等图片、样式表全部加载。此时用 DOMContentLoaded 更合适。
怎么想:如果代码只是查找元素、绑定事件,不依赖图片大小等资源,就可以在 DOM 解析完成后执行。
怎么做:当初始 HTML 文档被完全加载和解析完成之后触发,而无需等待样式表、图像等完全加载:
javascript
document.addEventListener('DOMContentLoaded', function() {
// xxxxx
})易错点:DOMContentLoaded 监听对象是 document,不是 window;它不等图片和样式表完全加载,所以不要在这里读取依赖图片资源的尺寸。
元素滚动事件 scroll:滚动条滚动时持续触发
为什么需要:很多网页需要检测用户把页面滚动到某个区域后做处理,比如固定导航栏、返回顶部。滚动不是一次性的,而是滚动过程中持续触发。
怎么想:先确定要监听“整个页面滚动”还是某个元素滚动。本节笔记给的是监听整个页面滚动。
怎么做:
javascript
window.addEventListener('scroll', function() {
// xxxxx
})易错点:scroll 会持续触发,回调里不要放特别重的逻辑;如果只是做显示隐藏或读取位置,也要注意触发频率。调试时可以先在回调里打印一句话,确认滚动时事件确实执行。
scrollTop / scrollLeft:读取和修改被卷去的距离
为什么需要:只知道“发生了滚动”还不够,通常还要知道滚到哪了,才能做固定导航、返回顶部等效果。
怎么想:scrollTop 表示被卷去的头部距离,scrollLeft 表示被卷去的左侧距离;它们可以读取,也可以修改,赋值会改变滚动位置。
怎么做:在 scroll 回调中读取或赋值,例如读取当前被卷去的头部距离,再根据数值做判断。笔记明确说明:scrollTop / scrollLeft 可以读取,也可以修改(赋值)。
易错点:不要把“被卷去的距离”理解成元素自身高度;读取和赋值是两种操作,赋值会真的改变滚动位置。调试时可用 console.log 打印数值,观察滚动前后的变化。
页面尺寸事件 resize:窗口尺寸改变时触发
为什么需要:窗口大小变化后,页面布局、可见区域宽高可能变化,需要重新处理。
怎么想:只要业务依赖“窗口尺寸变化”这个时机,就监听 resize。
怎么做:会在窗口尺寸改变的时候触发:
javascript
window.addEventListener('resize', function() {
// xxxxx
})易错点:resize 不是点击一次触发一次,而是尺寸改变过程中触发;回调里读取尺寸时,要区分 clientWidth/clientHeight 与带边框的尺寸概念。
clientWidth 和 clientHeight:获取可见部分宽高
为什么需要:做响应式判断、计算可见区域时,需要拿到元素可见部分的宽高。
怎么想:笔记说明 clientWidth 和 clientHeight 获取元素的可见部分宽高,不包含 border、margin、滚动条等。
怎么做:在 resize 回调或其他需要的位置读取元素的 clientWidth、clientHeight,再根据数值调整逻辑。
易错点:不要把 clientWidth/clientHeight 和包含边框的尺寸混用;它们不包含 border、margin、滚动条等。调试时可先打印数值,再和页面实际可见区域对照。
调试与验证:先确认触发,再看值
转写稿里反复出现“先拿到集合、再循环绑定、再打印验证”的调试思路;迁移到本节,就是先确认事件有没有触发,再确认读到的值对不对。比如在 load 回调里打印标记,在 scroll 回调里打印 scrollTop,在 resize 回调里打印 clientWidth。转写稿口述中的“尾数组”“缩引号”“艾特木”等是转写误差,对应应写作“伪数组”“索引号”“item”;读代码时以实际 API 为准。这样既验证了事件时机,也验证了属性读写是否符合预期。
课件图示








代码示例
代码一:事件解绑(移除事件处理函数)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>事件解绑</title>
</head>
<body>
<button class="l2">移除L2事件监听</button>
<button class="l0">移除L0事件监听</button>
<script>
// 需求:按钮只点击一次,然后移除点击事件
// 1. l2 事件监听
const l2 = document.querySelector('.l2')
l2.addEventListener('click', fn)
function fn() {
alert('我点击了')
// 移除事件监听
l2.removeEventListener('click', fn)
}
// 2. l0 事件监听
const l0 = document.querySelector('.l0')
l0.onclick = function () {
alert('我点击了')
// 移除事件监听
l0.onclick = null
}
</script>
</body>
</html>要点解释:
removeEventListener('click', fn)必须传入同一个函数引用,所以回调要写成具名函数fn,不能写成匿名函数(匿名函数解绑不了)。- L0 事件(
onclick)解绑直接把onclick = null即可;onclick会被覆盖,同名事件只能绑定一个处理函数。 - 易错点:解绑时事件类型、函数引用必须与绑定时完全一致,否则"解绑失败"且没有任何报错提示。
代码二:排他思想(点击谁,谁高亮)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>排他思想</title>
<style>
button {
width: 80px;
height: 40px;
margin-right: 10px;
}
/* 高亮样式类 */
.pink {
background-color: pink;
}
</style>
</head>
<body>
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<button>按钮5</button>
<script>
// 1. 获取所有按钮(多个,一定要用 querySelectorAll)
const btns = document.querySelectorAll('button')
// 2. 利用循环,一次性给 5 个按钮绑定点击事件
for (let i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', function () {
// 3. 先排除其他人:把别人身上的 pink 类移除
const pink = document.querySelector('.pink')
if (pink) pink.classList.remove('pink')
// 4. 再留下我自己:谁被点击,谁就是 this
this.classList.add('pink')
})
}
</script>
</body>
</html>要点解释:
- 口诀顺序不能颠倒:先移除其他人,再给自己添加。反过来的话,自己刚加上的类会立刻被移除,点击"没效果"。
document.querySelectorAll('button')返回伪数组,必须写All;btns[i]才是每个按钮对象。- 回调里用
this表示当前被点击的元素,比写btns[i]更直观;classList.add / remove操作类名不带点号。 - 易错点:页面初始没有
.pink元素时,直接querySelector('.pink').classList.remove()会报错(演示里也出现了这个报错),可先判断再移除。
代码三:tab 栏切换(鼠标经过高亮 + 底部跟随切换)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>tab栏切换</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.tab { width: 600px; margin: 50px auto; }
.tab-na { height: 40px; line-height: 40px; border-bottom: 1px solid #ccc; }
.tab-na li { float: left; list-style: none; }
.tab-na a {
display: block;
padding: 0 20px;
text-decoration: none;
color: #333;
}
/* 高亮:谁有 active 谁突出显示 */
.tab-na a.active {
background-color: pink;
color: #fff;
}
/* 底部内容:默认全部隐藏,谁有 active 谁显示 */
.tab-content .item { display: none; height: 200px; background: #f5f5f5; }
.tab-content .item.active { display: block; }
</style>
</head>
<body>
<div class="tab">
<div class="tab-na">
<ul>
<li><a href="javascript:;" class="active">精选</a></li>
<li><a href="javascript:;">美食</a></li>
<li><a href="javascript:;">百货</a></li>
<li><a href="javascript:;">客户</a></li>
<li><a href="javascript:;">预告</a></li>
</ul>
</div>
<div class="tab-content">
<div class="item active">精选的内容</div>
<div class="item">美食的内容</div>
<div class="item">百货的内容</div>
<div class="item">客户的内容</div>
<div class="item">预告的内容</div>
</div>
</div>
<script>
// 业务一:鼠标经过谁,谁高亮
// 1. 获取 5 个链接
const as = document.querySelectorAll('.tab-na a')
// 2. 获取底部 5 个大盒子(与上面的 a 一一对应)
const items = document.querySelectorAll('.tab-content .item')
// 3. 循环注册鼠标经过事件
for (let i = 0; i < as.length; i++) {
as[i].addEventListener('mouseenter', function () {
// 3.1 排他:先移除其他链接的 active
const oldA = document.querySelector('.tab-na .active')
if (oldA) oldA.classList.remove('active')
// 3.2 留下我自己
this.classList.add('active')
// 业务二:底部跟随切换
// 4.1 先把底部带 active 的盒子移除(全都隐藏)
const oldItem = document.querySelector('.tab-content .active')
if (oldItem) oldItem.classList.remove('active')
// 4.2 利用索引号 i,让对应的盒子添加 active 显示出来
items[i].classList.add('active')
})
}
</script>
</body>
</html>要点解释:
- 布局要求:上面几个小盒子,下面就对应几个大盒子(一一对应),切换靠"显示 / 隐藏"实现。
- 选择器要写全
.tab-na .active、.tab-content .active,因为上下都有active类,不加父级限定会选错元素。 items[i]的i就是鼠标经过的那个a的索引号,用它从伪数组里取出对应的大盒子,这就是"底部跟随"的核心。- 循环变量必须用
let,每次循环生成独立作用域;如果用var,回调执行时i已经是循环结束的值,取到的永远是最后一个。 - 鼠标经过用
mouseenter(不冒泡,经过一次只触发一次),mouseover在子元素间移动会反复触发。 - 易错点:先移除再添加的顺序不能反;
querySelector只取一个(排他场景够用),querySelectorAll才取全部。
常见错误
L2 解绑无效,点击仍持续触发
现象:调用removeEventListener后事件仍然生效。
原因:绑定时传匿名函数,解绑时又传了另一个匿名函数,函数引用不一致。
修法:绑定与解绑使用同一个具名函数或同一个保存下来的函数引用。L0 与 L2 解绑方式混用
现象:onclick绑定却用removeEventListener移除,或addEventListener绑定却写onclick = null。
原因:L0 与 L2 的事件绑定/解绑 API 不匹配。
修法:L0 用onclick = null,L2 用removeEventListener(type, fn)。script在head中直接获取 DOM 得到null
现象:报错Cannot read properties of null。
原因:HTML 尚未解析到目标元素,DOM 节点还不存在。
修法:把脚本放到body末尾,或使用DOMContentLoaded/load等待合适时机。排他思想顺序颠倒,高亮被自己移除
现象:点击/经过后当前元素没有高亮,或效果时有时无。
原因:先给当前元素添加类,再统一移除旧类,导致刚添加的类也被清掉。
修法:严格先移除其他元素的激活类,再给当前this添加激活类。Tab 栏内容切换时误选上方导航的激活类
现象:导航高亮消失、内容区显示错乱。
原因:querySelector('.active')未限定父容器,可能选中了另一区域的.active。
修法:分别限定范围,如.tab-nav .active与.tab-content .active。轮播图点击小圆点后不切换图片
现象:点击小圆点有事件,但调用切换函数无变化。
原因:只调用了切换函数,没有把当前小圆点索引同步给控制图片的信号量。
修法:先取当前小圆点索引并赋给信号量,再调用公共切换函数。循环绑定事件时索引混乱
现象:所有回调都拿到最后一个索引,或小圆点与内容不对应。
原因:循环变量作用域/命名混淆,事件回调中使用的外部索引不明确。
修法:使用let形成块级作用域,必要时用独立变量名区分“当前索引”和“播放信号量”。
练习
实现两个按钮:一个用 L2 绑定且只响应一次点击,另一个用 L0 绑定且只响应一次点击。要求写出解绑代码,并说明为什么解绑函数必须一致。
将
script放在head中获取页面按钮,分别用load和DOMContentLoaded测试触发时机;说明什么场景选load,什么场景选DOMContentLoaded。用排他思想实现 5 个按钮点击高亮:要求循环绑定、事件内使用
this、先移除其他按钮激活类再添加当前激活类,并故意测试顺序颠倒后的现象。完成 Tab 栏切换:鼠标经过导航项时当前项高亮,下方对应内容显示、其余隐藏。要求使用索引一一对应,并限定导航区与内容区的选择器范围。
在轮播图案例中增加“点击小圆点切换图片”:循环给小圆点绑定点击事件,点击后同步当前索引到播放信号量,再复用已有切换函数;测试点击小圆点后左右按钮和自动播放是否仍正常。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 排他思想 | 11:46 |
| 2 | 综合案例-tab栏切换原理分析 | 05:47 |
| 3 | 综合案例-tab栏切换鼠标经过高亮显示 | 07:31 |
| 4 | 综合案例-tab栏切换底部跟随显示 | 10:18 |
| 5 | 轮播图完整版-点击小圆点可以切换图片 | 08:36 |
小结
- 事件解绑的核心不是“再写一次移除语句”,而是绑定与解绑必须指向同一个函数引用。
- L0 与 L2 的绑定、解绑方式不能混用;选择哪种绑定方式,就用对应的解绑方式。
- 页面加载事件要按资源依赖选择:只等 DOM 用
DOMContentLoaded,等全部资源用load。 - 排他思想必须保持“先排除其他,再保留自己”的顺序,否则当前元素容易被误清除。
- 批量绑定事件优先用循环,但循环变量和事件内索引要清晰,避免所有回调拿到同一个索引。
- Tab 栏切换的关键是导航项与内容项索引一一对应,显示隐藏由激活类控制。
- 轮播图点击小圆点的关键动作是“同步索引/信号量,再复用切换函数”,而不是重复写一套切换逻辑。
- 选择器要限定父容器范围,避免不同模块的
.active互相误伤。