DOM 事件的几个易混点
改页面脚本时我反复踩到的七个点,每条先给结论,再配一段代码或一张表。
一、捕获与冒泡的顺序
结论:事件先从 window 向下捕获到目标,再向上冒泡回来,所以外层捕获先跑、内层冒泡先跑。第三个参数写 true 走捕获,省略或写 false 走冒泡,现在一般写成对象。
outer.addEventListener("click", onOuter, { capture: true }); // 先
inner.addEventListener("click", onInner); // 中
outer.addEventListener("click", onOuter2); // 后
// 目标元素自己的监听器不分阶段,只按注册顺序执行。
// 同阶段、同函数、同选项重复注册,第二次会被静默忽略。
二、target 与 currentTarget 不是一回事
event.target | event.currentTarget | |
|---|---|---|
| 指谁 | 被点到的那个最深层元素 | 正在执行监听器的元素 |
| 委托时 | 可能是 li,也可能是里面的 span | 始终是挂监听的 ul |
| 生命周期 | 全程不变 | 只在监听器执行期间有值 |
list.addEventListener("click", function (event) {
var item = event.target.closest("li"); // 从被点的元素往上找
if (item === null || list.contains(item) === false) {
return; // 点在列表外面,不管
}
console.log(event.currentTarget, item.dataset.id);
});
三、事件委托什么时候不该用
结论:子项多、会动态增删、处理器只做分发时适合委托;事件本身不冒泡、或要精细拦传播的不适用。
focus、blur不冒泡,委托要换成focusin、focusout,这条我踩得最久。scroll在元素上不冒泡,想在容器上委托滚动得开捕获。input、change、submit冒泡,能委托给表单。
form.addEventListener("focusin", function (event) {
if (event.target.matches("input, textarea")) {
showTip(event.target.name); // 换成 focus 这里永远收不到
}
});
四、passive 与 preventDefault 冲突
结论:标了 passive: true 就是承诺不阻止默认行为,再调 event.preventDefault() 不起作用,控制台还会报错;浏览器拿这个承诺去提前开始滚动。window、document、body 上的 wheel、touchstart、touchmove 默认就是被动的,我要拦滚动得显式关掉。
panel.addEventListener("wheel", onWheel, { passive: true }); // 只统计,不拦截
sheet.addEventListener("touchmove", onMove, { passive: false }); // 要 preventDefault 就写 false
五、preventDefault 与 stopPropagation 各拦什么
| 调用 | 拦住的东西 | 不拦的东西 |
|---|---|---|
preventDefault() | 默认动作:跳转、提交、滚轮滚动 | 照样冒泡,祖先的委托照样收到 |
stopPropagation() | 继续向外层传播 | 默认动作照常,链接照样跳 |
stopImmediatePropagation() | 同元素后面的监听器也不执行 | 无 |
我在 li 里点删除,外层 a 跟着跳转,就是只写了 stopPropagation,忘了拦跳转的是 preventDefault。
六、click 与 pointerdown 的时序差
结论:一次鼠标操作的顺序是 pointerover、pointerdown、pointerup、click;click 要等按下和抬起都完成,触屏还要多等一次手势判定。所以拖拽和即时反馈绑 pointerdown,确认意图绑 click,同一个动作别两个都绑,会执行两遍。
handle.addEventListener("pointerdown", startDrag); // 接触屏幕的瞬间
ok.addEventListener("click", commit); // 抬起之后才到这里
七、once 与解绑要盯住函数引用
结论:{ once: true } 只在事件真正触发时自动解绑,一直没触发它就还挂在原地;而 removeEventListener 靠「类型 + 函数引用 + 捕获选项」三者匹配,匿名函数永远删不掉,passive 不参与匹配。
function onScroll(event) { /* 收起顶部条 */ }
window.addEventListener("scroll", onScroll, { passive: true });
window.removeEventListener("scroll", onScroll); // 同一个引用,删得掉
window.removeEventListener("scroll", onScroll, true); // 捕获选项对不上,删不掉
一次性的交互我就写 { once: true },但函数照旧提到外面命名,防止它被注册第二遍。
备忘:整页只有一个脚本时这些都还不明显;等同一批元素上出现多个来源的监听器,差一层捕获就会变成 bug。