DOM 交互补充:事件委托、可见性与 rAF
系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)
- 第 13 篇:Promise 与 async/await
- 第 14 篇:数据结构基础
- 第 15 篇:垃圾回收与内存
- 第 16 篇:DOM 基础全面解析
- 第 17 篇:DOM 性能与渲染
- 第 18 篇:DOM 交互补充(本文)
文章目录
- 系列文章目录
- 前言
- 一、事件流:捕获与冒泡
- 二、`target` 与 `currentTarget`
- 三、事件委托(面试重点)
- 3.1 是什么
- 3.2 为什么用
- 3.3 局限
- 四、不冒泡时的替代
- 五、IntersectionObserver(可见性)
- 与 `loading="lazy"` 选型
- 六、`requestAnimationFrame`(rAF)
- 与 `setTimeout`、微任务的分工
- 七、了解即可(一笔带过)
- 八、易混淆点归纳
- 九、思考与练习
- 总结
前言
DOM 主线分三篇收尾:第 16 篇讲节点与 API,第 17 篇讲渲染与性能;本篇补交互层——用户点击、滚动、元素进出视口时 JS 怎么响应。面试最常问的是事件委托;工程里还会用到IntersectionObserver(懒加载、加载更多)和requestAnimationFrame(动画/滚动)。CustomEvent、passive等知道存在即可,不必单独成篇。
一、事件流:捕获与冒泡
DOM 事件传播:捕获(外→内)→ 目标 → 冒泡(内→外)。默认addEventListener在冒泡阶段触发。
outer.addEventListener("click",()=>console.log("outer 冒泡"));outer.addEventListener("click",()=>console.log("outer 捕获"),true);btn.addEventListener("click",()=>console.log("btn"));// 点击 btn:outer 捕获 → btn → outer 冒泡日常写业务冒泡阶段足够;捕获多用于拦截或框架内部,了解即可。
二、target与currentTarget
| 属性 | 含义 |
|---|---|
event.target | 实际触发事件的元素(最内层,可能是文本节点) |
event.currentTarget | 当前执行监听器的元素(addEventListener绑定的那个) |
委托时二者不同:绑在父级#list上,currentTarget始终是#list,target是具体子元素。
list.addEventListener("click",(e)=>{constli=e.target.closest("li");if(!li||!list.contains(li))return;console.log("点到 li:",li.dataset.id);});closest(selector):从target向上找匹配祖先,避免点到span/文本节点时对不上li。
三、事件委托(面试重点)
3.1 是什么
在祖先元素上绑一个监听器,利用冒泡处理多个子孙(含后来插入的节点)的同类事件。
3.2 为什么用
- 动态列表:新增/删除项不用反复
addEventListener/removeEventListener。 - 监听器更少:内存与注册成本更低(第 17 篇)。
- 逻辑集中:列表、表格、菜单等结构清晰。
constlist=document.querySelector("#list");list.addEventListener("click",(e)=>{constitem=e.target.closest("li");if(!item)return;item.classList.toggle("active");});document.querySelector("#add").addEventListener("click",()=>{constli=document.createElement("li");li.textContent="新项";list.appendChild(li);// 无需再绑 click});3.3 局限
- 依赖冒泡;
focus、mouseenter等不冒泡,不能这样委托。 - 需要
closest过滤,避免误触嵌套结构。
四、不冒泡时的替代
| 不冒泡 | 可冒泡替代 |
|---|---|
focus/blur | focusin/focusout |
mouseenter/mouseleave | mouseover/mouseout(会因子元素频繁触发) |
form.addEventListener("focusin",(e)=>{if(e.target.matches("input"))e.target.classList.add("focused");});scroll也不冒泡,需绑在滚动容器本身。
五、IntersectionObserver(可见性)
异步观察元素与**视口(或 root 容器)**的交叉状态,常用于:
- 图片懒加载(进入视口再设
src) - 无限滚动(底部哨兵进入视口加载下一页)
- 曝光统计(元素可见比例达标上报)
constio=newIntersectionObserver((entries)=>{entries.forEach((entry)=>{if(!entry.isIntersecting)return;constimg=entry.target;img.src=img.dataset.src;io.unobserve(img);});},{rootMargin:"100px",threshold:0.01});document.querySelectorAll("img[data-src]").forEach((img)=>io.observe(img));| 选项 | 作用(知道即可) |
|---|---|
root | 观察根,默认视口 |
rootMargin | 扩大/缩小触发区域,如提前 100px 加载 |
threshold | 可见比例 0~1 触发回调 |
与loading="lazy"选型
loading="lazy" | IntersectionObserver | |
|---|---|---|
| 成本 | 原生,零 JS | 需写逻辑 |
| 控制 | 浏览器决定 | rootMargin、回调自定义 |
| 场景 | 普通<img>懒加载 | 无限滚动哨兵、复杂曝光 |
建议:纯图片懒加载优先原生;加载更多、埋点用 IO。
六、requestAnimationFrame(rAF)
在下一次重绘前执行回调,与显示器刷新率对齐(约 60fps),适合动画循环、滚动中更新 UI。
letticking=false;window.addEventListener("scroll",()=>{if(ticking)return;ticking=true;requestAnimationFrame(()=>{updateHighlight();// 读 scrollTop、改 class 等ticking=false;});});与setTimeout、微任务的分工
| 机制 | 典型用途 |
|---|---|
微任务(Promise.then) | 异步结果、DOM 更新调度(第 08 篇) |
setTimeout | 延迟、防抖定时 |
| rAF | 视觉相关、跟帧动画/滚动 |
rAF不是精确定时器;后台标签页可能暂停或降频。精确计时应使用performance.now()+ 时间差,而非假设每帧 16ms。
动画属性优先transform/opacity(第 17 篇),在 rAF 里改它们更顺滑。
七、了解即可(一笔带过)
passive: true:告诉浏览器不会preventDefault,滚动更流畅;要阻止滚动需passive: false。once: true:监听一次后自动移除。CustomEvent:DOM 节点上派发自定义事件,模块解耦;复杂场景可用 EventBus(第 11 篇)。- React / Vue:框架在根或元素上统一处理事件;原生
addEventListener与框架onClick勿重复绑;卸载时记得清理。
八、易混淆点归纳
- 委托靠冒泡;
focus要用focusin。 target≠currentTarget;委托看target+closest。- IO 不是 scroll 事件;不阻塞主线程滚动,回调异步触发。
- rAF ≠ 微任务;别用 rAF 替代
Promise.then的语义。 loading="lazy"与 IO互补,不是二选一排斥。
九、思考与练习
1.动态 Todo 列表为何推荐委托而不是每项onclick?
解析:新增项不用绑事件;监听器O(1),维护简单。
2.点击li内文字,target可能是谁?如何拿到li?
解析:可能是Text 节点或内层元素;e.target.closest('li')。
3.图片首屏外懒加载,原生属性与 IO 如何选?
解析:简单<img loading="lazy">;要提前加载距离或加载更多哨兵用IO。
4.滚动监听里直接做重 DOM 操作为何卡?rAF 起什么作用?
解析:scroll 触发极频繁;rAF合并到每帧一次,且对齐重绘。
5.form.addEventListener('focus', ...)能委托到所有 input 吗?
解析:不能,focus不冒泡;用focusin。
总结
- 事件:捕获/冒泡;委托= 父级监听 + 冒泡 +
closest,适合动态列表。 - 可见性:IntersectionObserver做懒加载、无限滚动;简单图片可用
loading="lazy"。 - rAF:动画/滚动对齐帧;与微任务、定时器分工不同。
- DOM 三篇(16 基础 / 17 性能 / 18 交互)至此收束。
下一阶段进入CSS 布局:居中、BFC、Flex/Grid 等(系列后续篇目)。
