Skip to content

2-9事件流和事件委托 ​

本页对应 第二章WebAPIs,共 8 段视频,约 51 分钟。

学习目标 ​

  1. 能区分事件捕获与事件冒泡的执行顺序,并通过 addEventListener 第三个参数验证捕获、冒泡两种阶段。
  2. 能在事件回调中获取事件对象,使用 stopPropagation() 阻止事件冒泡,并验证子元素事件不再触发父元素同名事件。
  3. 能对比 mouseover/mouseout 与 mouseenter/mouseleave 的冒泡差异,并根据是否需要事件委托选择正确事件类型。
  4. 能解释事件委托的原理与优势,使用父元素绑定事件,并通过 e.target、e.target.tagName 筛选真正触发元素。
  5. 能使用事件委托完成 tab 栏切换:用 data-* 自定义属性保存索引,通过 dataset 读取序号,实现顶部高亮排他与底部内容显示排他。
  6. 能列举页面加载、页面滚动、页面尺寸事件及元素尺寸与位置获取的常用 API,并写出获取元素宽高或位置的基本代码。

知识点 ​

页面加载事件:load ​

为什么需要:页面里的图片、外联 CSS、外联 JavaScript 等资源加载需要时间。如果脚本在资源还没处理完时就去操作 DOM,容易拿不到元素。转写稿里反复使用 addEventListener 注册事件,本节沿用同一思路,只是把监听对象换成 window。

怎么想:load 等的是“页面所有资源加载完毕”。所以它适合做“资源全部处理完再执行”的事情,例如依赖图片尺寸的初始化。老代码常把 script 写在 head 中,直接找 DOM 会找不到,这时就需要等加载完成。

怎么做:

javascript
window.addEventListener('load', function() {
    // xxxxx
})

调试时可先在回调里写 console.log('load 触发'),确认触发时机后再写业务逻辑。转写稿中“补货”应纠正为“捕获”,“复盒子”应纠正为“父盒子”;本节不展开事件流,只保留正确的监听写法。

易错点:load 监听的是 window,不是随便一个元素;它等待资源较多,触发相对晚,不要把它当成“HTML 一解析完就执行”。

DOMContentLoaded ​

为什么需要:有时不需要等图片、样式表全部加载完,只要 HTML 文档被完全加载和解析完成,就要初始化 DOM。这样比 load 更早。

怎么想:DOMContentLoaded 的关键是“HTML 文档解析完成”,无需等待样式表、图像等完全加载。它适合在 DOM 结构就绪后绑定事件、查找元素。

怎么做:

javascript
document.addEventListener('DOMContentLoaded', function() {
    // xxxxx
})

易错点:监听对象是 document,不是 window;不要把它和 load 混用。转写稿里把 target 说成“他跟他”、把 tagName 说成“tiger name”,整理时应写成 target、tagName,注意驼峰命名。

元素滚动事件:scroll ​

为什么需要:很多网页要检测用户把页面滚动到某个区域后做处理,比如固定导航栏、返回顶部。转写稿提到页面滚动会出现吸附导航、电梯导航,这类效果都离不开滚动检测。

怎么想:滚动条滚动时会持续触发 scroll。监听整个页面滚动时,对象是 window。先确认“谁在滚”,再在回调里读取滚动位置。

怎么做:

javascript
window.addEventListener('scroll', function() {
    // xxxxx
})

易错点:scroll 会持续触发,回调里不要写过于复杂的逻辑;先打印或读取位置确认触发,再写业务。转写稿中的“补货”不是本节概念,不要误写成滚动事件的一部分。

scrollTop / scrollLeft ​

为什么需要:只知道“发生了滚动”不够,还要知道被卷去的头部或左侧距离,才能判断是否到达某个区域。

怎么想:scrollTop / scrollLeft 表示被卷去的头部或者左侧,可以读取,也可以修改(赋值)。读取用于判断位置,赋值可用于回到某个位置。

scrollTop / scrollLeft, 被卷去的头部或者左侧,可以读取,也可以修改(赋值)

易错点:读取和赋值要分清;不要把“卷去距离”当成元素宽高。调试时沿用转写稿里的 console.log 思路,先打印数值,确认单位与变化。

页面尺寸事件:resize ​

为什么需要:窗口尺寸改变时,页面布局可能需要重新计算或调整。监听 resize 可以在尺寸变化时执行处理。

怎么想:窗口尺寸改变时触发,监听对象是 window。先绑定事件,再在回调里读取需要的尺寸。

怎么做:

javascript
window.addEventListener('resize', function() {
    // xxxxx
})

易错点:resize 可能频繁触发,回调逻辑要尽量轻;不要把 resize 和 scroll 混淆。

clientWidth 和 clientHeight ​

为什么需要:有时要获取元素可见部分的宽高,用于布局判断或尺寸计算。

怎么想:clientWidth 和 clientHeight 获取元素的可见部分宽高,不包含 border、margin、滚动条等。

clientWidth和clientHeight,获取元素的可见部分宽高(不包含border,margin,滚动条等)

易错点:不要把 clientWidth 当成包含边框的宽度;它和 scrollTop 不是一类属性。转写稿中“GS”应写为“JS”,“mo over / mo enter”应写为 mouseover / mouseenter,本节只保留其他事件相关写法,不扩展未讲内容。

先说明:本节笔记与转写稿的对应关系 ​

本节笔记的主题是“元素尺寸与位置”,核心结论只有一句:获取元素的自身宽高时,读到的是包含元素自身设置的宽高、padding、border 的尺寸。转写稿中,这一节只出现在今日内容总览里:提到“最后我们会学习元素的尺寸与位置,可以通过 JS 形式拿到标签的大小,还有它在页面中的位置是多少”,并引出后续“吸附导航”“电梯导航”等案例。转写稿没有展开这一节的代码演示和调试过程,因此下面不虚构运行结果,只按笔记结论把知识点讲清;转写稿里“GS”应写作“JS”,“菜标栏/电梯大行”应理解为“侧边栏/电梯导航”。

元素尺寸包含自身宽高、padding、border 示意图

元素尺寸与位置相关属性示意图

为什么需要元素尺寸与位置 ​

为什么需要:只写 CSS 时,盒子大小由样式决定;但一旦进入交互,比如页面滚动后出现吸附导航、点击电梯导航跳到某个模块,JS 就必须知道“这个元素现在实际占多大”“它在页面中的位置在哪里”。笔记里说的“自身宽高、包含 padding、border”,正是实际占位尺寸,而不是你在 CSS 里写的那一个 width 数字。

怎么想:先区分“样式值”和“实际占位值”。style.width 读的是行内样式里写的值,可能是 "200px",也可能为空;元素实际占位则要把内容宽、padding、border 一起算进去。所以读尺寸时不能只盯着 width。

怎么做:按笔记结论,读取元素自身宽高时,要认识到结果包含自身设置的宽高、padding、border。这类读取是“读结果”,不是“改样式”。

易错点:不要把 CSS 的 width 直接等同于元素实际占位宽;不要以为读了尺寸就能修改尺寸,尺寸读取属性通常是只读的。

怎么想:盒模型决定“读到的是哪一圈” ​

为什么需要:如果不知道读的是盒模型的哪一圈,就会把 padding、border 漏掉,导致拖拽、定位、吸顶判断出现几像素偏差。

怎么想:把元素想成一个盒子:最里面是内容,外面一圈是 padding,再外面是 border,最外面还有 margin。笔记明确说“包含元素自身设置的宽高、padding、border”,说明它读的是盒子占位,不含 margin。所以判断时先问:我要的是内容区,还是整个盒子占位?笔记这一节要的是后者。

怎么做:看示意图时按“内容 + padding + border”去数;写代码前先确认 CSS 里有没有 padding、border,否则容易误判。

易错点:把 margin 也算进去是常见错误;margin 是盒子外面的间距,不属于笔记所说的自身宽高包含范围。

怎么做:用尺寸属性读取占位尺寸 ​

为什么需要:后续做吸附导航、电梯导航时,需要拿真实占位尺寸做判断,不能靠猜。

怎么想:笔记给的是结论,没有给代码。按结论,读取元素自身宽高时应使用能返回“包含 padding、border 的占位尺寸”的属性;学习时先记住“读实际占位,不读样式字符串”。

怎么做:先获取元素,再读取它的尺寸结果;得到的是数值型结果,单位按像素理解。由于转写稿没有给这一节的具体代码,这里不补造代码,避免与课堂实际写法冲突。你只需把握:读尺寸是“获取”,不是“设置”。

易错点:元素被 display:none 隐藏时,尺寸读不到实际占位;小数像素在不同环境可能被取整;只读属性不能直接赋值修改。

位置部分:笔记只点到,不臆造具体 API ​

为什么需要:标题里有“位置”,转写稿也提到“页面中的位置是多少”,说明位置和尺寸是一组能力。做电梯导航时,必须知道模块在页面中的位置,才能滚到对应区域。

怎么想:位置一定要先问“相对谁”。相对页面、相对定位父级、相对视口,结果可能不同。笔记没有列出具体位置属性名,转写稿也没有展开,所以这里不能编造具体方法。

怎么做:本节先建立“尺寸是盒子占多大,位置是盒子在哪里”的区分;具体读取方式等后续案例或课堂代码展开。不要因为标题有“位置”就随便套一个属性。

易错点:把尺寸当位置、把相对父级的位置当页面位置,都会导致滚动定位偏差;位置计算前要先确认定位参照物。

小结 ​

本节要抓住一句话:元素自身宽高的读取结果包含自身设置的宽高、padding、border,不包含 margin。为什么需要:交互案例要拿真实占位;怎么想:按盒模型从内容到 border 数一圈;怎么做:读实际占位而不是读 style.width;易错点:漏 padding/border、多算 margin、把只读结果当可写样式。位置部分笔记只点到,转写稿未展开,学习时先分清“相对谁”,不臆造具体 API。

课件图示 ​

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

课件第48页:获得元素的尺寸大小和页面中的位置 l 使用场景: Ø 可以通过js的方式,得到元

课件第49页:4. 元素尺寸与位置 大小 位置 l offsetWidth 和 offsetH

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

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

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

课件第11页:1.4 阻止冒泡 l 问题:因为默认就有冒泡阶段的存在,所以容易导致事件影响到父

课件第14页:l 事件委托(Event Delegation):是JavaScript中注册事

代码示例 ​

代码 1:页面加载 / 滚动 / 尺寸事件 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>页面加载 / 滚动 / 尺寸事件</title>
  <style>
    body { height: 2000px; margin: 0; }
    .box {
      width: 200px; height: 100px;
      padding: 10px; border: 5px solid #333; margin: 20px;
      background: pink;
    }
    .back-top {
      position: fixed; right: 30px; bottom: 30px;
      display: none;
    }
  </style>
</head>
<body>
  <div class="box">clientWidth / clientHeight</div>
  <button class="back-top">返回顶部</button>

  <script>
    // ① load:页面所有外部资源(图片、外联 CSS/JS)加载完毕才触发
    window.addEventListener('load', function () {
      console.log('load:所有资源加载完毕')
    })

    // ② DOMContentLoaded:HTML 文档被完全加载和解析完成就触发,不等图片
    document.addEventListener('DOMContentLoaded', function () {
      console.log('DOMContentLoaded:DOM 解析完成')
      console.log(document.querySelector('.box')) // 此时能拿到元素
    })

    // ③ scroll:滚动条在滚动的时候持续触发
    const backTop = document.querySelector('.back-top')
    window.addEventListener('scroll', function () {
      const n = document.documentElement.scrollTop // 被卷去的头部,可读取
      console.log('scrollTop:', n)
      backTop.style.display = n >= 300 ? 'block' : 'none'
    })

    // scrollTop 也可以修改(赋值)
    backTop.addEventListener('click', function () {
      document.documentElement.scrollTop = 0
    })

    // ④ resize:窗口尺寸改变的时候触发
    window.addEventListener('resize', function () {
      const box = document.querySelector('.box')
      console.log('clientWidth:', box.clientWidth, 'clientHeight:', box.clientHeight)
    })
  </script>
</body>
</html>

关键行与易错点

  • load 监听的是 window,等图片、外联 CSS/JS 全部加载完;DOMContentLoaded 监听的是 document,DOM 解析完就触发,不等图片。老代码把 script 写在 head 里找不到 DOM 元素,就是靠这两个事件包起来解决的。
  • 读取整个页面被卷去的头部用 document.documentElement.scrollTop;scrollTop 既可读取也可赋值,赋值 0 就回到顶部。
  • clientWidth / clientHeight 获取元素可见部分宽高,包含 padding,不包含 border、margin、滚动条——这一点最容易记反。
  • scroll 事件在滚动时持续触发,回调里不要写太重的逻辑。

代码 2:事件流——冒泡与阻止冒泡、mouseover 与 mouseenter ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>事件冒泡与阻止冒泡</title>
  <style>
    .father { width: 300px; height: 300px; background: pink; }
    .son { width: 150px; height: 150px; background: purple; }
  </style>
</head>
<body>
  <div class="father">
    <div class="son"></div>
  </div>

  <script>
    const father = document.querySelector('.father')
    const son = document.querySelector('.son')

    // 冒泡阶段:第三个参数省略或写 false(默认)
    father.addEventListener('click', function () {
      console.log('我是爸爸')
    })
    son.addEventListener('click', function (e) {
      console.log('我是儿子')
      // 阻止冒泡:先拿到事件对象 e,再调用 stopPropagation(方法要加括号)
      e.stopPropagation()
    })

    // mouseover 有冒泡,mouseenter 没有冒泡
    father.addEventListener('mouseover', function () {
      console.log('mouseover:经过了爸爸')
    })
    son.addEventListener('mouseover', function () {
      console.log('mouseover:经过了儿子')
    })
    father.addEventListener('mouseenter', function () {
      console.log('mouseenter:经过了爸爸')
    })
    son.addEventListener('mouseenter', function () {
      console.log('mouseenter:经过了儿子')
    })
  </script>
</body>
</html>

关键行与易错点

  • 点 .son 会先打印“我是儿子”,再打印“我是爸爸”,这就是冒泡:从里到外依次执行同名事件;捕获则相反(从外到里),靠事件监听第三个参数传 true 开启,开发中很少用。
  • 阻止冒泡必须先拿到事件对象(回调函数的第一个参数),再调用 e.stopPropagation();它是方法,必须加括号,写成 e.stopPropagation 无效。
  • mouseover 有冒泡:鼠标从父盒子移到子盒子时,父盒子的 mouseover 会再触发一次;mouseenter 没有冒泡,只触发一次。所以事件委托必须用 mouseover。
  • 演示时用 console.log,用 alert 会阻塞页面,连续弹出时容易误判执行顺序。

代码 3:事件委托版 tab 栏切换 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>事件委托版 tab 栏切换</title>
  <style>
    * { margin: 0; padding: 0; }
    li { list-style: none; }
    .tab { width: 600px; margin: 50px auto; }
    .tab-nav { display: flex; border-bottom: 1px solid #ccc; }
    .tab-nav li { flex: 1; text-align: center; }
    .tab-nav a {
      display: block; padding: 10px 0;
      color: #333; text-decoration: none;
    }
    .tab-nav a.active { background: pink; color: #fff; }
    .tab-content .item {
      display: none; height: 150px; padding: 20px;
      background: #f5f5f5;
    }
    .tab-content .item.active { display: block; }
  </style>
</head>
<body>
  <div class="tab">
    <ul class="tab-nav">
      <li><a href="javascript:;" data-id="0">精选</a></li>
      <li><a href="javascript:;" data-id="1">美食</a></li>
      <li><a href="javascript:;" data-id="2">百货</a></li>
      <li><a href="javascript:;" data-id="3">客户预告</a></li>
      <li><a href="javascript:;" data-id="4">最新专题</a></li>
    </ul>
    <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. 事件委托:把事件注册在父元素 ul 上,只注册一次
    const ul = document.querySelector('.tab-nav')
    const items = document.querySelectorAll('.tab-content .item')

    ul.addEventListener('mouseover', function (e) {
      // 2. 只有经过的是 A 链接才处理(tagName 必须大写)
      if (e.target.tagName === 'A') {
        // 3. 排他:先移除上一个 active
        const prevA = document.querySelector('.tab-nav .active')
        const prevItem = document.querySelector('.tab-content .active')
        if (prevA) prevA.classList.remove('active')
        if (prevItem) prevItem.classList.remove('active')

        // 4. 当前元素高亮:事件委托里用 e.target,不要用 this
        e.target.classList.add('active')

        // 5. 通过自定义属性 data-id 拿到序号,显示对应盒子
        const index = e.target.dataset.id
        items[index].classList.add('active')
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • 绑在 ul 而不是每个 li:li 有 5 个,ul 只有一个,注册一次就够,减少注册次数;点击 a 后靠冒泡冒到 ul 上被处理,这就是事件委托。
  • 必须用 mouseover:事件委托依赖冒泡,mouseenter 没有冒泡,委托不生效。
  • e.target.tagName === 'A' 中标签名是大写字符串,写 'a' 永远不成立;加这个判断是为了排除点到 ul、li 等非链接元素。
  • 事件委托里获取当前触发元素用 e.target,不能用 this(this 指向绑定的 ul)。
  • 序号通过自定义属性 data-id 提供,读取用 e.target.dataset.id(data- 后面的名字转小驼峰);拿到序号后直接用 items[index] 选中对应盒子。
  • 排他前先判断 prevA / prevItem 是否存在,避免 querySelector 返回 null 时调用 classList 报错。

常见错误 ​

1. head 里直接 querySelector,拿到 null,后续全报错

  • 现象:Cannot read properties of null (reading 'addEventListener')
  • 原因:script 在 head 中同步执行时,下方 DOM 还没解析出来
  • 修法:优先把 script 放 </body> 前;必须放 head 就包一层 document.addEventListener('DOMContentLoaded', ...);若逻辑依赖图片等资源尺寸,改用 window.addEventListener('load', ...)

2. 用 mouseenter / mouseleave 做事件委托,子元素毫无反应

  • 现象:事件绑在 ul 上,鼠标划过 li 不触发任何逻辑
  • 原因:mouseenter/mouseleave 不冒泡,委托的传递链从根上就断了
  • 修法:委托场景改用 mouseover/mouseout,并在回调里用 e.target.tagName 过滤;若坚持用 mouseenter,就放弃委托、循环绑定到每个子元素

3. 阻止冒泡写了但没生效

  • 现象:子元素回调里加了 stopPropagation,父元素照样触发
  • 原因:三种典型写法问题——写成 e.stopPropagation(漏括号没调用)、写在父元素的回调里、把 e 参数漏在函数形参外
  • 修法:function (e) { e.stopPropagation(); ... },位置放在真正触发事件的那个元素(子元素)的回调首行

4. 委托里用 this 取当前元素,拿到的是父元素

  • 现象:点击 li,变色/高亮作用在 ul 上(表现为整块全变)
  • 原因:this 指向"绑定事件的元素",不是"被点击的元素"
  • 修法:委托场景一律用 e.target;结构有嵌套(如 a 里套 span)时用 e.target.closest('a') 兜底

5. 委托没加 tagName 判断,点空白区域也触发

  • 现象:点 ul 的间隙、点 li 里的子标签,逻辑误触发或整片变色
  • 原因:e.target 是"实际点到的任意节点",不等于你想要的 li/a
  • 修法:加守卫 if (e.target.tagName === 'LI') { ... },标签名必须是大写字符串;更稳的写法是 if (!e.target.matches('.tab a')) return

6. 排他思想里 classList 带了点号

  • 现象:classList.remove('.active') 静默失败,多个高亮叠加
  • 原因:classList.add/remove 接收类名,不接收选择器
  • 修法:remove('active')、add('active') 不带点;只有 querySelector('.active') 才带点

7. dataset 读写对不上

  • 现象:e.target.dataset.ID 或 dataset.dataId 得到 undefined
  • 原因:data-id → dataset.id,data-goods-id → dataset.goodsId(连字符转驼峰,全小写起头)
  • 修法:HTML 端统一 data-id="0",JS 端统一 dataset.id;下标用 items[e.target.dataset.id] 前先确认该属性存在

8. 尺寸/滚动 API 选错

  • 现象:scrollTop 永远 0;clientWidth 比设计稿少 2px
  • 原因:滚动的不是被监听对象(页面滚 window,容器滚要自身 overflow:auto 且有高度);clientWidth 不含 border,也不含滚动条
  • 修法:先问"谁在滚"再决定监听谁;要含 border 用 offsetWidth,要视口宽用 window.innerWidth

练习 ​

练习 1|委托入门加固 给一个含 5 个 li 的 ul,实现"点击哪个 li,哪个 li 文字变红",要求:事件只注册 1 次;点击 ul 空白处无任何反应;点击后能拿到被点 li 的文本内容并 console.log。 验收:在控制台点击 ul 的 padding 区域不打印任何东西;连点不同 li,红色只有一个。 提示:e.target.tagName === 'LI'、e.target.textContent。

练习 2|mouseover 与 mouseenter 对照实验 父子两个嵌套盒子,先用 mouseover/mouseout 绑定并打印,再用 mouseenter/mouseleave 绑定并打印,各跑一遍记录日志条数与顺序,写 3 行注释说明差异原因。 验收:能解释"为什么 over 那组在进入子盒子时会多打印一次父盒子的日志";能回答"这组实验说明了委托能不能用 enter"。

练习 3|吸顶 + 返回顶部(含 scrollTop 边界) 做一个长页面:滚动超过 300px 时导航吸顶、返回顶部按钮出现;点击按钮回到顶部。要求用 window 的 scroll 事件,并用一个布尔开关做简单节流。 验收:刷新后不滚动时按钮隐藏;滚回顶部后按钮自动隐藏;连续快速滚动不掉帧、控制台不刷屏。 提示:document.documentElement.scrollTop、window.scrollTo(0, 0)。

练习 4|tab 栏切换(事件委托 + 自定义属性) 把原来的 for 循环版 tab 改造为委托版:事件绑在 ul 上,5 个 a 手动写 data-id="0~4",底部 5 个 div 用 items[序号] 切换。要求上、下两个模块都用排他思想,且点击 tab 区域空白不变。 验收:鼠标划过每个 tab,高亮与内容一一对应且不串位;把 HTML 里某个 data-id 故意改错,能说出会发生什么现象。 提示:mouseover + tagName === 'A' + dataset.id。

练习 5|尺寸响应式小判断 用 resize + 视口宽度,实现窗口宽度小于 768px 时给 body 加 mobile 类、大于等于则移除;同时用 clientWidth 与 offsetWidth 各打印一次某个带 border 的盒子宽度,观察差值。 验收:拖动窗口边界时类名能正确增删且不重复叠加;能说出两个宽度值差多少、差在哪。

原视频对照 ​

#视频时长
1今日内容01:53
2事件流11:23
3阻止事件冒泡03:22
4mouseover和mouseenter的区别04:21
5事件委托的原理06:47
6利用事件委托得到目标元素07:48
7事件委托版本-tab栏切换上08:37
8事件委托版本-tab栏切换下07:06

小结 ​

  1. 绑定时机的问题优先于绑定写法的问题:能放 body 末尾就别依赖 load,DOMContentLoaded 解决 DOM 就绪,load 只留给"必须等资源"的场景。
  2. 冒泡是默认行为,不是异常:先默认接受它,只有父子逻辑确实要断开时才 stopPropagation,滥用会连带打死委托和"点空白关闭弹层"这类全局监听。
  3. 捕获是知识,冒泡是工具:第三个参数 true 知道即可,业务代码基本不写;但要知道"为什么点子元素会触发父元素"这个问题的答案在冒泡。
  4. 选事件先看它冒不冒泡:要委托就选 click / mouseover / input 这类冒泡事件,mouseenter / mouseleave 天生不适合委托,这是选型而非技巧问题。
  5. 委托三件套固定套路:父元素上绑一次 → e.target 定位真正触发者 → tagName / matches / closest 做过滤守卫,缺了第三步代码就不严谨。
  6. 委托里 this 和 e.target 是两个东西,前者是绑定的父元素、后者是点到的元素,改造旧代码时把 this 换成 e.target 是最容易漏的一步。
  7. 尺寸 API 按三问选:含不含 border(offset vs client)、含不含滚动条、是元素还是视口(innerWidth);滚动类问题永远先确认"谁在滚"。
  8. data-* + dataset 是 DOM 与 JS 之间的数据通道,比靠索引、靠文本内容、靠位置关系取数据都稳,改造循环版逻辑时它是第一选择。