先记住这个答案
一次事件从按钮传播到父容器时,各监听器拿到的是同一次派发的事件对象,只是 currentTarget 随当前监听器变化。下一次用户点击应当作为新的事件处理,不能把上次事件对象当成会自动更新的容器。脚本创建的 Event 可以保存,在前一次派发完成后再次 dispatchEvent();但在它尚在派发时重入会报错,而且旧的取消状态等信息可能造成困惑,通常每次操作新建对象更清楚。
- 同一次传播共享同一事件对象
- 独立用户操作是不同事件
currentTarget只在回调期间有意义- 自建事件不能在派发中重入派发
异步读取 currentTarget 的坑
列表点击时准备稍后上报“哪个容器处理了事件”。若把整个 event 留到 Promise 回调里再读 currentTarget,派发已经结束,它通常是 null。需要的节点和 ID 应在同步监听器里先取出。
const list = document.querySelector('#items')
list.addEventListener('click', event => {
const container = event.currentTarget // 监听器执行期间读取
const id = event.target.closest('[data-id]')?.dataset.id
queueMicrotask(() => {
console.log({ id, containerId: container.id })
})
})微任务里用的是已经保存的 container 和 id,不依赖事件派发结束后的动态上下文。event.target 可能仍可访问,但目标节点也可能随后被删除,保存稳定业务 ID 更可靠。
同一次传播里,父子监听器看到的 currentTarget 不同。不能把它当成事件最初发生的位置;原始目标看 target,跨 Shadow DOM 时还要考虑重定向。
自建事件对象可以再次派发,但别滥用
脚本可创建一个 Event 后先派发到 A,再在前一次派发结束后派发到 B。第二次派发不是“浏览器复用用户点击对象”,而是你的代码主动把同一个对象再次交给事件系统。
DOM 规范禁止同一对象在派发尚未结束时再次派发。即使顺序派发允许,事件上的状态与引用仍可能让业务逻辑难懂;通常每次业务动作创建一个新事件对象。框架的合成事件还可能有自己的包装规则,不应混同于原生 DOM 事件。
例如一个可取消的自建事件第一次派发时被监听器调用了 preventDefault(),之后复用同一个对象再派发,已有取消状态可能影响第二次的判断。与其手动重置并猜测哪些状态会保留,不如用相同参数新建 Event;事件对象本身只负责描述这一回发生的事,业务数据另存。
在异步逻辑里保留事件引用也不是绝对错误,但应只读取稳定且确实需要的字段。currentTarget 是派发期间的上下文,派发结束就不适合再读;目标元素虽然还能引用,却可能已经离开文档。把 id、坐标和必要状态复制到普通对象里更容易复核。
容易答错的地方
- 认为事件对象一过回调就不能保存
- 对象引用仍可保存,但
currentTarget等派发时上下文不保证在回调外仍有值。 - 认为两个连续真实点击必定共享一个对象
- 每次独立用户操作应按独立事件处理,不能依赖对象身份复用。
面试官还会怎么问?
异步上报最应该提前保存什么?
保存业务 ID、必要坐标及当前监听容器等稳定数据,别在异步回调里重新读 currentTarget。
能在自己的监听器里再次派发同一个事件吗?
不能在该对象仍处于派发中时重入;会触发状态错误。
React 的合成事件与原生事件一样吗?
它是框架层包装,映射和生命周期可能不同;这里讨论的是原生 DOM Event。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。