【问题标题】:How/when do event listeners get attached in d3.js?事件侦听器如何/何时附加到 d3.js 中?
【发布时间】:2017-01-20 16:31:54
【问题描述】:

我正在尝试制作一个 SVG 编辑器。长话短说,我需要将鼠标事件附加到给定 SVG 中特定深度的 <g> 元素。由于各种原因,我无法提前知道 ID。 SVG 非常庞大,将包含数百个元素。

d3.selectAll("svg > g > g > g").select("g").on("mouseover", function() {
    console.log("mouseover");
  }).on("mouseout", function() {
    console.log("mouseout");          
  }).on("click", function() {
    console.log("clicked");
  });

此代码有效,但需要很长时间才能启动。假设我有十个这样的元素可以匹配那个特定的选择。似乎在页面加载后的每一秒,10 个中的另一个实际上都附加了鼠标事件。我想知道是否可以在 d3 每次附加事件时打印控制台事件,或者我如何判断 d3 是否已完成附加它需要附加的所有内容。

基本上这个JSFiddle 需要更快地加载鼠标事件。如果你等待几秒钟,你会看到越来越多的盒子在工作。

【问题讨论】:

  • 你说的是什么版本的 D3?你能用 JSFiddle 举个例子吗?
  • @tiblu 我使用的是版本 4
  • @tiblu 添加了小提琴
  • 您可能需要通过将鼠标事件添加到根 <svg> 元素并使用您自己的命中检测逻辑来针对这种情况进行优化。
  • 这太奇怪了。我不认为这是d3 的问题。如果您使用straight JavaScript you get the same outcome 附加事件处理程序。

标签: javascript d3.js svg dom-events


【解决方案1】:

tl;博士

事实证明,这是臭名昭著的pointer-eventsfill 麻烦的复杂变体。事件处理程序实际上立即附加到<g> 元素。但是,它们在一段时间内不会执行,因为事件在大多数情况下都不会传递到这些元素。设置pointer-events: all 可以轻松解决此问题。

除了技术问题之外,这是一个完美的例子,说明了为什么您应该提供一个minimal示例,其中的东西被精简到最低限度。大量的代码使得攻击变得不必要地困难。以下 sn-p 包含足够的代码来演示该问题:

d3.select("g").on("mouseover", function() {
  // The difference between below log entries shows, that the event was
  // targeted at another element and bubbled up to this handler's element.
  console.dir(d3.event.target.tagName);   // <rect>: actual target for this event
  console.dir(this.tagName);              // <g>:    element this handler is attached to

  d3.select(this).select("rect")
    .style("fill", "orange");
});
rect {
  stroke: red;
  stroke-width: 0.2;
  stroke-dasharray: 1.5 1.5;
  fill:none;
}
<script src="https://d3js.org/d3.v4.js"></script>
<svg width="300" height="300">
  <g>
    <rect x="20" y="20" width="200" height="200"/>
  </g>
</svg>

分析

当浏览器确定哪个元素将成为指针事件的目标时,它会做一些称为命中测试的事情:

16.5.1 Hit-testing

确定指针事件是否导致正面命中测试取决于指针的位置、graphics element 的大小和形状以及元素上‘pointer-events’ 属性的计算值。

上面的句子包含两条关于您的问题的重要信息:

  1. 只有图形元素可以成为指针事件的直接目标,而仅&lt;g&gt; 元素本身不能成为这些事件的目标。然而,这些事件可能会冒泡并最终到达该群体。在您的事件处理程序中,您可以记录在d3.event.targetthis 中引用的事件的实际目标,它指向该处理程序附加到的元素:

    .on("mouseover", function() {
      // The difference between below log entries shows, that the event was
      // targeted at another element and bubbled up to this handler's element.
      console.log(d3.event.target);   // <path>: actual target for this event
      console.log(this);              // <g>:    element this handler is attached to
    
      d3.select(this).select("path")
        .style("fill", "orange");
    })
    

    正如您在JSFiddle 中看到的,这些总是不同的。 这与您的方案相关,因为您在组上注册了处理程序函数。这样,只有当组的图形子元素成为指针事件的目标并且事件冒泡到组本身时,处理程序才会被执行。这本身并不是什么大问题,但是结合下一点,这解释了为什么您的设置不起作用。

  2. pointer-events 属性确定,“元素是否或何时可能成为鼠标事件的目标”。因为这个属性从来没有在你的代码中设置,所以默认值是visiblePainted,定义如下(强调我的):

    只有当可见性属性设置为可见并且鼠标光标位于元素的内部(即“填充”)并且设置了填充属性时,该元素才能成为鼠标事件的目标设置为 none 以外的值,或者当鼠标光标位于元素的周边(即“stroke”)上且 stroke 属性设置为 none 以外的值时。

    正如其他人在 cmets 中指出的那样,您组中的相关 &lt;path&gt; 元素都具有定义 fill: none 的类 st8,从而防止它们在悬停其内部时成为事件目标,即填充。当这些路径不能成为指针事件的目标时,就没有事件可以冒泡到您的组,这会使事件侦听器无用。

    如果第一次在一个元素上执行了监听器(为什么会发生这种情况,下面会解释,所以暂时请耐心等待),这个问题可以通过设置fill 属性自行解决在路径上,从而使其成为指针事件的合法目标。这就是为什么处理程序在它们刚开始运行时会继续运行的原因。

    旁注:这种效果非常强大,甚至会影响开发工具在 Chrome 和 Firefox 中处理这些元素的方式。当您尝试检查已将填充设置为无的元素时,通过右键单击它,开发工具将打开引用根 &lt;svg&gt; 元素而不是您单击的元素,因为后者不是事件的目标.相比之下,使用事件处理程序已经在工作的元素试试这个,可以这么说,它会为这个元素打开开发工具。

解决方案

对此的简单解决方案是通过将属性显式设置为all,允许指针事件发生在路径的内部,即填充路径:

只有当指针位于内部(即填充)或周边(即, 行程)的元素。 fillstrokevisibility 属性的值不影响事件处理。

这最好在我更新的JSFiddle 中注册事件处理程序之前完成:

d3.selectAll("svg > g > g").select("g").select("g")
  .attr("pointer-events", "all")
  .on("mouseover", function() {
    //...
  }

为什么它有时会起作用以及为什么会出现延迟?

上面提供了一个正确的分析和一个可行的解决方案,但是,如果你给它一些时间来深入了解,仍然存在一个问题,为什么处理程序似乎被注册了,或者,至少,在这样的延迟下被激活。对此进行了更多思考,结果发现我的解释中已经包含了理解该问题的所有信息。

正如我上面所说,&lt;path&gt; 元素实际上是事件目标,而不是组。 pointer-events 属性默认为 visiblePainted,它们对于指针事件并非完全不可访问,如重新阅读上述规范所示:

[...] 或者当鼠标光标位于元素的周界(即“stroke”)上并且 stroke 属性设置为 none 以外的值时。

尽管臭名昭著的类st8 设置了stroke: ff0000(显然不是没有),但它指定了stroke-width:0.24,这是一条非常细的线。事实证明,除了虚线之外,根本很难达到目标。但是,如果您真的点击它,它将导致路径成为事件目标,事件冒泡到组,最终执行事件处理程序。可以通过将stroke-width 设置为更大的值以更容易命中路径来演示此效果:

.st8 {
  fill:none;
  stroke:#ff0000;
  stroke-dasharray:1.68,1.2;
  stroke-linecap:round;
  stroke-linejoin:round;
  stroke-width:2      /* Set to 2 to make it easier to hit */
}

看看这个JSFiddle 的工作演示。

即使没有设置pointer-events: all,这也会起作用,因为线条现在足够宽,可以被指针击中。因为粗线条很难看,并且会破坏精细的布局,但这更像是一个演示而不是真正的解决方案。

【讨论】:

  • 这很棒。感谢您的解释,但是就提供最少的代码而言...我尝试使用精简的 SVG 并且问题不再存在(尽管现在知道解决方案...这可能是因为我创建了在默认为更宽的行的编辑器中剥离 svg)
  • @Ben 我已经编辑了答案并包含了一个minimal reproducible example。我同意,要减少这么多需要做很多工作,但是必须有人去做;-) 而且,根据我自己的经验,我可以肯定地说,构建一个最小的示例通常有助于找到自己解决。无论如何,我喜欢深入研究这个问题,我可能是从中学到最多的人 ;-)
【解决方案2】:

这是一个非常有趣的问题,我设法使它起作用,但我无法解释为什么会起作用。如果有深入了解的人能解释这一点,将不胜感激。

慢:

var targetElements = d3.selectAll("svg > g > g").select("g").select("g").select("path");
targetElements.on("mouseover", function() {
  d3.select(this)
    .style("fill", "orange");
}).on("mouseout", function() {
  d3.select(this)
    .style("fill", "BLUE");
}).on("click", function() {
  d3.select(this)
    .style("fill", "green");
});

快速:

var targetElements = d3.selectAll("svg > g > g").select("g").select("g").select("path");
targetElements.style('fill', 'white'); // Black magic - comment this out and the event handler attachment is delayed alot
targetElements.on("mouseover", function() {
  d3.select(this)
    .style("fill", "orange");
}).on("mouseout", function() {
  d3.select(this)
    .style("fill", "BLUE");
}).on("click", function() {
  d3.select(this)
    .style("fill", "green");
});

区别仅在于在我将事件处理程序附加到元素之前对元素应用填充 - .style("fill", "white").on("mouseover",

玩的小提琴 - https://jsfiddle.net/v8e4hnff/1/

注意:还尝试在 SVG 元素上使用 JS 原生选择器和事件处理程序附件来实现,这比 D3 快一点。在 IE11 和 Chrome 上的行为是相同的。

如上所述,如果有人可以解释这种行为,请做!

【讨论】:

  • 尝试更新样式:.st8 {fill:#FFF;,它应该可以修复它。你不能mouseover空填充
  • @AlvinK。谢谢您的答复!更改 .st8 填充确实有效,mouseover 有效,但“你不能 mouseover 空填充”并不完全正确 - 如果您检查 Fiddle,一段时间后,.st8 { fill: none } 上的 mouseover 将启动在职的。我不确定工作是否合乎逻辑 - 未填充的元素是否会触发事件。
  • 哇,这太棒了。这里肯定有一些时髦的东西在幕后发生。我想知道 javascript 和/或浏览器是否必须经过大量额外的计算才能找出没有背景的东西的边界?
  • @Ben 这里没有什么特别时髦的东西;这只是一个众所周知的变相问题。我添加了自己的答案,这也解释了为什么这个答案的方法有助于解决问题。
  • 无法将鼠标悬停在空的填充物上真是太疯狂了
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多