491 字
2 分钟
03 · 事件机制
一次点击走了三步
点击一个按钮,事件不是直接打到它,而是沿 DOM 树三阶段传播:
- 捕获阶段:从
window向下到目标父级。 - 目标阶段:到达被点的元素。
- 冒泡阶段:从目标向上回到
window。
el.addEventListener("click", handler, true); // true=捕获阶段触发el.addEventListener("click", handler); // 默认冒泡阶段触发事件委托:一个监听管一片
给 1000 个 <li> 各绑监听既费内存又难维护。利用冒泡,在父容器上绑一个,靠 event.target 判断谁被点:
list.addEventListener("click", e => { const li = e.target.closest("li"); if (li) handle(li);});好处:动态增删子项无需重新绑事件,性能与可维护性双赢。这是 React 事件系统底层思路的来源之一。
addEventListener 的进阶选项
el.addEventListener("scroll", onScroll, { passive: true }); // 告诉浏览器我不会 preventDefaultel.addEventListener("click", once, { once: true }); // 只触发一次后自动解绑passive: true 关键:移动端 touchmove/wheel 监听若调用 preventDefault 会阻塞滚动。标记为 passive 后浏览器可放心异步滚动,消除卡顿(Chrome 对部分事件已默认 passive)。
止损与注意
- 委托时记得判断
target是否真是你想要的元素(closest兜底)。 - 组件卸载要
removeEventListener解绑,否则内存泄漏。React 的useEffect清理函数就是干这个。 - 阻止冒泡用
e.stopPropagation(),但别滥用——会打断上层逻辑(如全局点击关闭弹窗)。
小结
- 事件三阶段:捕获 → 目标 → 冒泡。
- 事件委托靠冒泡 +
closest,一个监听管全部子项。 passive让滚动事件不阻塞;once自动解绑。- 卸载时解绑监听,防止内存泄漏。
练习
- 在父容器上用事件委托处理动态列表点击,对比”每项是绑”的内存差异。
- 给
wheel监听加passive: true,用 Performance 看滚动是否仍流畅。 - 在捕获阶段和冒泡阶段各绑一个监听,点击目标元素,观察打印顺序。