一次 DOM 事件会先由外层祖先节点的 capture listener 向 target 下行。到达 target 后,注册在该节点的 capture listener 会先于 bubble listener 运行,虽然两者的观察都属于 target 阶段。如果事件支持冒泡,bubble listener 接着由 target 沿祖先节点向上运行。按这个顺序阅读观察点,就能找出最早可能改变或截断路径的 listener。
target 表示 listener 所看到的事件起点,currentTarget 则表示该条观察中正在运行 listener 的节点。composedPath() 提供当前运行环境可见的有序节点路径;本 trace 从 target 向根节点展示。阅读 capture 时反向看,阅读 bubble 时正向看。遇到 Shadow DOM 时,可见的 target 可能被重新定向,路径也可能省略 closed shadow 内部,因此应以记录到的路径为准,不要假设它等同于普通 DOM 的 parent 链。
stopPropagation 会阻止事件继续前往后续节点。同一 currentTarget、同一次 capture 或 bubble 运行轮次中的其他 listener 仍可运行;但如果 target 上的 capture listener 调用它,接下来 target 的 bubble 轮次会被跳过。stopImmediatePropagation 连同一轮次尚未运行的 listener 也会阻止。一次性 probe 会记录停止调用的方法、阶段与 currentTarget,因此应将停止点和最后一条观察对照,不必只靠注册代码猜测。修改 handler 后,再捕获一次相同事件,确认路径是否已到达预期的祖先节点。
关于这个主题的常见疑问与实用解答。
capture listener 由外层祖先节点向 target 运行。在 target 上,capture listener 先于 bubble listener 运行,两者都会报告为 target 阶段。如果 bubbles 为 true,bubble listener 再从 target 沿祖先节点向上运行。同一节点、同一类型的 listener 之间,则按注册顺序运行。
先检查 event.bubbles,因为有些事件类型本来就不会冒泡。如果其值为 true,再查找父元素之前是否调用了 stopPropagation 或 stopImmediatePropagation,确认父元素确实位于 composedPath(),并检查 Shadow DOM 或框架边界是否改变可见路径。trace 会展示路径、最后观察点与记录到的停止调用,可用来区分这些情况。
event.target 是 listener 所看到的事件起点,通常在传播过程中保持不变;event.currentTarget 是当前正在运行 listener 的节点,因此会随每条观察改变。使用事件委托时,用 target 判断被点击的后代节点,用 currentTarget 判断拥有 handler 的元素。
不一定。同一 currentTarget、同一次 capture 或 bubble 运行轮次中的其他 listener 仍可运行;但如果 target 上的 capture listener 调用 stopPropagation,接下来 target 的 bubble 轮次会被跳过。stopImmediatePropagation 除了停止后续传播,也会阻止同一轮次尚未运行的 listener。probe 会记录实际调用的方法,避免混淆这些情况。
选择事件类型,复制一次性 probe,并在发生问题的页面 DevTools 中运行。重现一次事件后,把生成的 JSON trace 粘贴回本工具。probe 会在捕获后移除 listener 并恢复临时修改的方法,只记录这一个事件,也不会通过网络发送 trace。
事件传播描述一个 DOM 事件沿哪些节点移动,以及路径上的 capture、target、bubble listener 如何运行;事件循环则通过调用栈、microtask 与 macrotask 决定 JavaScript 工作何时运行。一次 click 会作为任务进入事件循环;开始派发该事件后,事件传播才决定这次事件内部的 listener 顺序。