以下是 Matthias Christen 和 Florian Müller 来自 Ghostlab 的客座文章。Ghostlab 是适用于 Mac 和 PC 的跨浏览器跨设备测试软件。我非常印象深刻的一点是 Ghostlab 可以将一个浏览器的事件同步到所有其他浏览器。滚动一个页面,其他你正在测试的页面也会滚动。在一个页面上点击某个地方,相同的点击会在其他页面上发生。我问了他们,当有这么多可能干扰因素时,它究竟是怎么做到的。Matthias 和 Florian 解释道
我们一直在开发 Ghostlab,它是一款用于跨设备和跨浏览器测试网站的工具。本质上,它会同步查看网站的任何数量的客户端,并使您能够轻松地浏览该网站,并确保它在所有视窗和平台上都能正常工作且看起来很好。一个核心组件是跨客户端的事件复制——当用户在一个客户端上点击按钮、滚动或在表单字段中输入文本时,我们必须确保在所有其他客户端上也发生完全相同的事情。
捕捉漏掉的事件
Ghostlab 的客户端脚本组件正在监听所有发生的事件,尝试捕捉它们,并将它们复制到所有其他客户端。在某个时候,我们注意到我们并没有完全捕捉到所有事件。我们必须弄清楚问题出在哪里,并找到了一个解决方案,它允许您捕捉到您网站上发生的任何事件,无论它们如何被任何自定义 JavaScript 处理。
怎么可能您正在监听一个事件,但没有捕捉到它呢?这是因为任何事件处理程序都可以选择对一个事件执行多种操作。您会知道阻止浏览器通常执行的默认操作的能力(preventDefault()
)。例如,它允许您有一个链接(<a>
),对于该链接,用户不会在 click
事件上转到其 href
。
除了告诉浏览器在每次事件发生时不要执行默认操作外,事件处理程序还可以阻止事件传播。当在一个元素(比如一个链接)上触发一个事件时,任何附加到此特定元素的事件处理程序将被允许首先处理该事件。处理完后,事件将向上冒泡,直到到达 document
级别。此事件在原始元素的任何父元素上的任何监听器都将能够对该事件做出反应——除非较低级别的事件处理程序决定停止传播,在这种情况下,事件将不再向上遍历 DOM。
我们的 示例 1 演示了这一点。当您点击内部 div(Level 3)时,此元素的点击处理程序将首先处理该事件。除非它阻止传播,否则父元素(Level 2、Level 1)随后将能够按顺序对该事件做出反应。如果您选中了“停止传播”复选框,事件处理程序将阻止进一步传播——因此,Level 3 上的点击事件将不再到达 Level 1 和 Level 2,Level 2 上的点击事件将不再到达 Level 1。
查看 CodePen 上 Florian Mueller (@mueflo00) 的 事件传播示例 I。
在 示例 2 中,我们演示了停止立即传播的效果。此方法隐式地阻止了事件的冒泡,因此如果有任何父元素,我们会观察到与示例 1 相同的行为。此外,它还阻止了在同一元素上对同一事件的任何其他处理程序执行。在我们的示例中,我们必须在我们的元素上注册点击事件处理程序。如果我们选择停止立即传播,只有第一个响应者将能够处理该事件,并且在调用 stopImmediatePropagation 之后,将不再调用任何其他处理程序。
查看 CodePen 上 Florian Mueller (@mueflo00) 的 事件传播示例 II。
因此,如果您想监听 DOM 中发生的 tất cả 事件,这非常困难。为了防止由于取消冒泡而导致的事件丢失,您必须在 DOM 的每个元素上注册所有事件处理程序。即使这样,如果开发人员选择停止立即传播,这也只有在您是第一个注册该事件的人时才会起作用。
如果我们想要绝对确定我们被告知任何事件,无论其处理程序对其做了什么,我们都必须在事件注册的开始阶段进入循环。为此,我们覆盖了 EventTarget
对象的 addEventListener
函数。基本思路很简单:任何事件处理程序注册最终都会调用此方法。如果我们覆盖它,我们就完全控制了任何事件处理程序注册时发生的事情。
原始 addEventListener
函数将事件处理程序函数(“原始事件处理程序”)作为其第二个参数。如果我们不覆盖 addEventListener 函数,则原始事件处理程序函数将在每次发生指定事件时被调用。现在,在我们自定义的 addEventListener
函数中,我们只需将原始事件处理程序包装在我们自己的事件处理程序(“包装器函数”)中。包装器函数包含我们需要的任何逻辑,并最终可以调用原始事件处理程序——如果我们希望这样做。
示例 3 演示了这一点。“Level” 元素上附加的三个点击事件是通过我们自定义的 addEventListener
函数注册的,因此每当在这些元素上发生点击事件时,我们的包装器函数就会被调用。在那里,我们观察复选框的状态——如果它被选中,我们 simply 不调用原始事件处理程序,从而阻止任何点击事件触发任何原始事件处理程序。一个小小的注意事项:如果您想确保您控制所有事件,您必须确保在注册任何事件之前覆盖 addEventListener
函数。
查看 CodePen 上 Florian Mueller (@mueflo00) 的 事件覆盖示例。
虽然这个解决方案帮助我们改进 Ghostlab,但您可能会问自己,这有什么用呢?好吧,有几种可能性。我们下面概述了两种可能的用例——如果您能想出其他用例,请分享!
可能的应用 1:事件可视化器
有一些工具可以帮助您可视化网站上的事件(例如,我们非常喜欢 VisualEvent Allan Jardine)。使用我们的技术,我们可以快速地自己实现这样的工具。在我们的示例中,对于每个注册的事件,我们 simply 在注册该事件的元素的顶部绘制一个小方块。悬停时,我们会显示(原始)注册的事件处理程序函数的源代码。
查看 CodePen 上 Florian Mueller (@mueflo00) 的 事件可视化器。
可能的应用 2:事件统计
您也可以以其他方式显示事件指标,而不是将它们绘制到屏幕上。此示例向您展示了任何页面上注册事件的表格概述(假设您将代码注入其中),并实时更新触发的事件。例如,当您遇到性能问题并怀疑可能是因为事件处理程序过多时,这会很有用。
查看 CodePen 上 Florian Mueller (@mueflo00) 的 事件覆盖:统计。
事件只是前端开发巨大而复杂世界中的一小部分。在 Vanamco,我们很高兴能为您提供帮助简化和简化您的流程的工具,同时让您了解最佳实践。
它让我想起了 Jasmine Spies。
覆盖 addEventListener 很危险。不要建议这种技术。实际上,监听特定类型的 tất cả 事件非常简单。只需使用事件捕获。将第三个参数设置为 true
document.documentElement.addEventListener('click', function(){
//如果发生点击,此函数将始终被调用,
//即使在事件目标上使用了 stopImmediatePropagation
}, true);
是的,我一直想知道为什么他们甚至没有提到这种方法。我用过它,并且更喜欢它而不是包装内置的浏览器 API。我猜 GhostLab 的方法很好(?)因为它会捕获所有事件,包括尚未定义的事件。
我同意这是最好的解决方案。GhostLab 提出的解决方案并不适用于所有情况
document.body.onclick = function () {};
,实际上很危险。虽然演示“事件统计”无法使用 useCapture 实现。对主要问题的描述如下
事实上,这个问题并不难解决,使用事件捕获非常容易。此外,还有许多事件根本不会冒泡,按照规范,事件捕获是唯一可以与这类事件一起使用事件委托的方法。
虽然鸭子打洞 addEventListener 仍然可以在本文中出现。我真的很感激,如果这篇文章能够得到纠正,因为它明显错误,没有阅读评论的开发人员可能会学到不好的东西。
100% 同意 alexander farkas 的观点,你应该修改这篇文章。
非常感谢您的反馈并提及事件捕获。我们应该在文章中讨论这种可能性。如果您只是想监听发生的事件,这种方法可能更好更安全。
我们介绍的两个示例应用程序略有不同,因为它们不仅想监听事件,而且尽可能地控制事件的监听和处理方式。Ghostlab 也是如此。您提到我们的方法很危险——是的,没错,但我们所做的事情从一开始就危险:我们把我们的脚本注入到我们一无所知的网站中,并尝试启用我们自己的功能,同时不破坏它们。
我们认为,根据您愿意承担的风险以及您要完成的任务,不同的方法都是有效的,没有绝对错误的方法。
@Florian
在编程中,存在事实和观点。我以为我已经明确表达了,但我会尝试重复一些要点
你说得对。您提供的示例无法使用事件捕获实现,如果您想完全控制所有事件监听器本身,则必须修补 addEventListener(和 removeEventListener)。这既不好也不坏,这是唯一的方法。
但本文的主要问题描述(包括您的标题)和您的主要声明:“由于 stopPropagation 的存在,使用简单的全局事件监听器监听所有事件很困难”,被证明是错误的,或者充其量是误导性的。这与观点无关,这是一个事实。
我并不关心您是否只是想写一篇关于您产品的营销文章,并将其包含在肤浅的技术文章中。我关心的是这篇文章的教育意义。
这篇文章仍然可以具有一定的技术和教育价值,如果只需要更改 5-8 行代码。我认为,我可以从像 css-tricks 这样高价值的网站上期待这些。
“GhostLab”是否能像 BrowserStack 一样做?“GhostLab”会直接与“BrowserStack”竞争,这样人们就可以在其他浏览器中测试而无需支付月费吗?或者我弄错了,它们做的事情不同吗?
这个想法不是与 BrowserStack 竞争,Ghostlab 和 BrowserStack 可以协同工作。Ghostlab 可以同步任何连接到同一网络的客户端,例如移动设备、平板电脑或浏览器。您必须安装或访问设备或浏览器,这有很多优势。
addEventListener
无法捕获这个事件据我所知,iPad Safari 上没有定义 EventTarget。
您说得对——我们提供的代码不是生产代码,需要进行调整以支持所有可能的客户端。我们想更多地关注展示概念,而不是提供可以在任何地方运行的代码。
在 iPhone 上的 Safari 上不起作用!
SCRIPT5009: 'EventTarget' 未定义