Skip to content

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 为准。这样既验证了事件时机,也验证了属性读写是否符合预期。

课件图示 ​

课件第54页:描述 属性/方法 效果 事件流 addEventListener(‘事件类型’,

课件第26页:两种注册事件的区别 l 传统on注册(L0) Ø 同一个对象,后面注册的事件会覆

课件第10页:总结 总结 1. 事件流是什么?分为哪几个阶段? Ø 事件流指的是事件完整执行过

课件第25页:2. 移除事件监听(了解) 移除事件处理函数,也称为解绑事件 移除L2事件监听

课件第6页:1.1 事件流 l 为什么要学习事件流? Ø 可以帮我们解决一些疑惑,比如点击子

课件第7页:1.1 事件流 事件流指的是事件完整执行过程中的流动路径 当触发事件时,会经历两

课件第8页:1.2 事件捕获 l 事件捕获需要写对应代码才能看到效果 l 代码: l 说明:

课件第9页:1.3 事件冒泡 事件冒泡概念: 当一个元素的事件被触发时,同样的事件将会在该元

代码示例 ​

代码一:事件解绑(移除事件处理函数)

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 才取全部。

常见错误 ​

  1. L2 解绑无效,点击仍持续触发
    现象:调用 removeEventListener 后事件仍然生效。
    原因:绑定时传匿名函数,解绑时又传了另一个匿名函数,函数引用不一致。
    修法:绑定与解绑使用同一个具名函数或同一个保存下来的函数引用。

  2. L0 与 L2 解绑方式混用
    现象:onclick 绑定却用 removeEventListener 移除,或 addEventListener 绑定却写 onclick = null。
    原因:L0 与 L2 的事件绑定/解绑 API 不匹配。
    修法:L0 用 onclick = null,L2 用 removeEventListener(type, fn)。

  3. script 在 head 中直接获取 DOM 得到 null
    现象:报错 Cannot read properties of null。
    原因:HTML 尚未解析到目标元素,DOM 节点还不存在。
    修法:把脚本放到 body 末尾,或使用 DOMContentLoaded / load 等待合适时机。

  4. 排他思想顺序颠倒,高亮被自己移除
    现象:点击/经过后当前元素没有高亮,或效果时有时无。
    原因:先给当前元素添加类,再统一移除旧类,导致刚添加的类也被清掉。
    修法:严格先移除其他元素的激活类,再给当前 this 添加激活类。

  5. Tab 栏内容切换时误选上方导航的激活类
    现象:导航高亮消失、内容区显示错乱。
    原因:querySelector('.active') 未限定父容器,可能选中了另一区域的 .active。
    修法:分别限定范围,如 .tab-nav .active 与 .tab-content .active。

  6. 轮播图点击小圆点后不切换图片
    现象:点击小圆点有事件,但调用切换函数无变化。
    原因:只调用了切换函数,没有把当前小圆点索引同步给控制图片的信号量。
    修法:先取当前小圆点索引并赋给信号量,再调用公共切换函数。

  7. 循环绑定事件时索引混乱
    现象:所有回调都拿到最后一个索引,或小圆点与内容不对应。
    原因:循环变量作用域/命名混淆,事件回调中使用的外部索引不明确。
    修法:使用 let 形成块级作用域,必要时用独立变量名区分“当前索引”和“播放信号量”。

练习 ​

  1. 实现两个按钮:一个用 L2 绑定且只响应一次点击,另一个用 L0 绑定且只响应一次点击。要求写出解绑代码,并说明为什么解绑函数必须一致。

  2. 将 script 放在 head 中获取页面按钮,分别用 load 和 DOMContentLoaded 测试触发时机;说明什么场景选 load,什么场景选 DOMContentLoaded。

  3. 用排他思想实现 5 个按钮点击高亮:要求循环绑定、事件内使用 this、先移除其他按钮激活类再添加当前激活类,并故意测试顺序颠倒后的现象。

  4. 完成 Tab 栏切换:鼠标经过导航项时当前项高亮,下方对应内容显示、其余隐藏。要求使用索引一一对应,并限定导航区与内容区的选择器范围。

  5. 在轮播图案例中增加“点击小圆点切换图片”:循环给小圆点绑定点击事件,点击后同步当前索引到播放信号量,再复用已有切换函数;测试点击小圆点后左右按钮和自动播放是否仍正常。

原视频对照 ​

#视频时长
1排他思想11:46
2综合案例-tab栏切换原理分析05:47
3综合案例-tab栏切换鼠标经过高亮显示07:31
4综合案例-tab栏切换底部跟随显示10:18
5轮播图完整版-点击小圆点可以切换图片08:36

小结 ​

  1. 事件解绑的核心不是“再写一次移除语句”,而是绑定与解绑必须指向同一个函数引用。
  2. L0 与 L2 的绑定、解绑方式不能混用;选择哪种绑定方式,就用对应的解绑方式。
  3. 页面加载事件要按资源依赖选择:只等 DOM 用 DOMContentLoaded,等全部资源用 load。
  4. 排他思想必须保持“先排除其他,再保留自己”的顺序,否则当前元素容易被误清除。
  5. 批量绑定事件优先用循环,但循环变量和事件内索引要清晰,避免所有回调拿到同一个索引。
  6. Tab 栏切换的关键是导航项与内容项索引一一对应,显示隐藏由激活类控制。
  7. 轮播图点击小圆点的关键动作是“同步索引/信号量,再复用切换函数”,而不是重复写一套切换逻辑。
  8. 选择器要限定父容器范围,避免不同模块的 .active 互相误伤。