01事件流深潜

06 章学过「怎么绑事件」。这一课回答更扎心的问题: 为什么点了两次?为什么阻止默认没用?列表项动态添加后点不动了? 把事件流三阶段、委托、once/passive 一次吃透——这三件事决定了你写的交互稳不稳。

约 15 分钟 4 个动手实验 含排错 FAQ

一、事件流三阶段(必须能画出来)

1捕获 Capture

从 window → document → … → 目标父级。默认不触发你绑定的监听(除非第三参为 true)。

2目标 Target

事件到达真正被点的元素。event.target 就是它。

3冒泡 Bubble

从目标向上传回 window。你平时绑的 addEventListener 就在这阶段跑。

口诀:捕获向下、目标命中、冒泡向上。 90% 的业务只关心冒泡;写组件库、自定义弹层时才用捕获。
点内层按钮,看三阶段打印顺序:① 捕获从外到内;② 目标;③ 冒泡从内到外

二、target vs currentTarget

属性是谁典型用途
event.target 真正被点的元素 事件委托:知道点的是哪个 li
event.currentTarget 绑监听的那个元素 在父级监听时拿父级自己
this(普通函数) 同 currentTarget 箭头函数里没有,用 currentTarget
委托写法里 this 是父容器,不是被点的子项。 要拿子项请用 e.target 或 e.target.closest('.item')。

三、事件委托:动态列表的正解

给 1000 个 li 各绑 click,内存和绑定成本都高; 后来 appendChild 新增的项还点不动。 委托(delegation)只在父容器绑一次,靠 target 判断点到谁。

① 先用「逐个绑定」看动态项点不动;② 切到委托模式,新增项也能点;③ 对比两种写法

四、stopPropagation vs preventDefault

方法干什么不干什么常见误用
stopPropagation() 不再向上/下传播 不阻止默认行为 想拦表单提交却只写了它
preventDefault() 阻止默认行为(跳转/提交/选中) 不阻止冒泡 想拦父级监听却只写了它
stopImmediatePropagation() 连同元素后续监听也不跑 — 误以为和 stopPropagation 一样
return false(jQuery) 两者都干 原生 JS 里无效 从 jQuery 迁过来踩坑
外层 + 内层链接:① 只 preventDefault —— 冒泡仍在;② 只 stopPropagation —— 链接仍会跳转;③ 两个都写

五、once 与 passive:现代监听选项

passive: true 之后再调 preventDefault() 会被忽略并告警。 滚动 / 触摸相关监听尽量标 passive,列表滚动会明显更跟手。

六、常见问题 · 自问自答

Q

为什么我的点击回调跑了两次?

A

三种常见原因:① 父子都绑了 click,冒泡各跑一次 → 用 stopPropagation 或只在父级委托; ② 同一函数被 addEventListener 绑了两次(React 热更新 / 重复初始化)→ 用 {once:true} 或先 remove; ③ 同时绑了捕获和冒泡(第三参 true/false 各一次)→ 检查选项对象。

Q

为什么 preventDefault() 没拦住表单提交 / 链接跳转?

A

① 监听绑在了 submit 却写了 click 的 preventDefault——表单要用 submit 事件; ② 监听绑太晚,原生提交已经发生——保证 type="submit" 按钮在外层 form 内,并在 submit 上拦; ③ 监听选项写了 passive: true,preventDefault 被忽略。

Q

为什么动态 appendChild 的按钮点了没反应?

A

事件是绑在「当时已存在」的元素上的,新节点身上没有监听。两条路: ① 创建后立刻 btn.addEventListener; ②(推荐)改事件委托,只绑稳定的父容器。

Q

e.target 和 this 在委托里怎么选?

A

this / e.currentTarget = 绑监听的父级; e.target = 真正被点的元素(可能是图标 span)。 要稳定拿到「行 / 卡片」,用 e.target.closest('.item'),比 e.target 更抗嵌套。

七、排错清单(打印不出来时按顺序查)

  1. 事件类型拼写:click ≠ Click;表单是 submit 不是 click。
  2. 元素是否已存在:脚本是否在 defer / DOMContentLoaded 之后跑。
  3. 是否被 stopPropagation 截胡:临时在 window 上绑一个 log 看有没有到顶。
  4. 是否重复绑定 / 已 removeEventListener(引用必须同一个函数)。
  5. 遮挡:透明元素 / 伪元素挡住了点击(DevTools → 选中元素看 hit target)。

八、本章小结

  • 三阶段:捕获向下 → 目标 → 冒泡向上;默认监听在冒泡。
  • target 是被点的;currentTarget/this 是绑监听的。
  • 动态列表用事件委托 + closest。
  • preventDefault 管默认行为;stopPropagation 管传播——别混。
  • { once, passive, capture } 是现代默认写法。
动手练习: ① 做一个「点谁谁变红」的列表(只能委托实现,新增项也要能点); ② 嵌套三层 div,打印完整捕获+冒泡顺序,和本课实验对照; ③ 表单 submit 时 preventDefault,把数据 console.table 出来。