简体   繁体   English

如何/何时将事件侦听器附加到d3.js中?

[英]How/when do event listeners get attached in d3.js?

I am trying to make an SVG editor of sorts. 我正在尝试制作各种SVG编辑器。 Long story short, I need to attach mouse events to <g> elements at a specific depth within a given SVG. 长话短说,我需要将鼠标事件附加到给定SVG中特定深度的<g>元素上。 For various reasons I cannot know the ID ahead of time. 由于各种原因,我无法提前知道ID。 The SVG is huge and will have hundreds if not thousands of elements. 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");
  });

This code works, but it takes a long time before it gets started. 该代码有效,但是开始之前需要很长时间。 Let's say I have ten such elements that will match that particular selection. 假设我有十个这样的元素将与该特定选择匹配。 It seems like each second after page load another one of the 10 actually gets the mouse events attached. 似乎在页面加载后的每一秒中,实际上十个鼠标中的另一个实际上都附加了鼠标事件。 I am wondering if I can get a console event printed each time d3 attaches an event or how I can tell if d3 is done attaching everything it needs to attach. 我想知道是否可以在d3每次附加事件时打印控制台事件,或者我如何知道d3是否附加了它需要附加的所有内容。

Basically this JSFiddle needs to load the mouse events much more quickly. 基本上,这个JSFiddle需要更快地加载鼠标事件。 If you wait a few seconds you will see more and more boxes working. 如果您等待几秒钟,您将看到越来越多的盒子在工作。

tl;dr tl; dr

As it turns out, this is an intricate variation of the infamous pointer-events vs. fill hassle. 事实证明,这是臭名昭著的pointer-eventsfill麻烦的复杂变化。 The event handlers are in fact attached to the <g> elements right away. 实际上,事件处理程序会立即附加到<g>元素。 They are, however, not executed for some time, because the events will not get through to these elements most of the time. 但是,它们会在一段时间内不执行,因为事件在大多数情况下不会进入这些元素。 Setting pointer-events: all does easily fix this issue. 设置pointer-events: all确实可以轻松解决此问题。

Apart from the technical issues this is a perfect example of why you should provide a minimal example, where things are stripped down to the bare minimum. 除了技术问题之外,这还是一个很好的例子,说明了为什么您应该提供一个最小的例子,即把事情简化到最低限度。 The sheer amount of code made it unnecessarily hard to attack. 大量的代码使不必要的攻击变得困难。 The following snippet contains just enough code to demonstrate the issue: 以下代码片段仅包含足以说明问题的代码:

 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> 


Analysis 分析

When a browser determines which element will become a target of a pointer event, it will do something called hit-testing: 当浏览器确定哪个元素将成为指针事件的目标时,它将执行称为命中测试的操作:

16.5.1 Hit-testing 16.5.1命中测试

Determining whether a pointer event results in a positive hit-test depends upon the position of the pointer, the size and shape of the graphics element , and the computed value of the 'pointer-events' property on the element. 确定指针事件是否导致正面命中测试取决于指针的位置, 图形元素的大小和形状以及该元素'pointer-events'属性的计算值。

The above sentence contains two pieces of vital information for your issue: 上面的句子包含有关您的问题的两个重要信息:

  1. Only graphic elements can become direct targets of pointer events, whereas mere <g> elements alone cannot by itself be targets of these events. 只有图形元素可以成为指针事件的直接目标,而仅<g>元素本身不能成为这些事件的目标。 The events may, however, bubble and eventually reach that group. 但是,这些事件可能会冒出泡沫并最终到达该组。 From within your event handlers you can log the actual target of the event as referenced in d3.event.target as well as this , which points to the element, this handler was attached to: 在事件处理程序中,您可以记录d3.event.target引用的事件的实际目标以及this ,它指向该处理程序附加到的元素:

     .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"); }) 

    As you can see in this JSFiddle , these will always differ. 如您在此JSFiddle中所看到的,它们将始终不同。 This is relevant for your scenario, because you register the handler functions on the groups. 这与您的方案有关,因为您在组上注册了处理程序功能。 This way, the handlers will only get executed if a graphics child element of the group becomes a pointer event's target with the event bubbling up to the group itself. 这样,只有当组的图形子元素成为指针事件的目标并且事件冒泡到组本身时,处理程序才会执行。 This, on its own, is not much of a problem, but, in conjunction with the next point, this explains, why your set-up is not working. 就其本身而言,这不是什么大问题,但是结合下一点,这可以解释为什么您的设置无法正常工作。

  2. The pointer-events property determines, "whether or when an element may be the target of a mouse event" . pointer-events属性确定“元素是否或何时成为鼠标事件的目标” Because this property is never set throughout your code, the default kicks in, which is visiblePainted defined as follows (emphasis mine): 由于此属性从未在您的代码中设置,因此默认值即插即用,其visiblePainted定义如下(强调我的意思):

    The element can only be the target of a mouse event when the visibility attribute is set to visible and when the mouse cursor is over the interior (ie, 'fill') of the element and the fill attribute is set to a value other than none , or when the mouse cursor is over the perimeter (ie, 'stroke') of the element and the stroke attribute is set to a value other than none. 仅当可见性属性设置为visible且鼠标光标位于元素的内部(即“填充”)且fill属性设置为除none之外的其他值时,该元素才可以是鼠标事件的目标。 ,或者当鼠标光标悬停在元素的边界(即“笔触”)上并且笔触属性设置为除none之外的其他值时。

    As others have noted in the comments the relevant <path> elements within your group all feature the class st8 which defines fill: none , whereby preventing them from becoming an event target when hovering their interior, ie, fill. 正如其他人在评论中指出的那样,组中所有相关的<path>元素均具有st8类, st8定义fill: none ,从而防止它们在悬停其内部(即fill)时成为事件目标。 When these paths cannot become target for the pointer events, there is no event, that could bubble up to your group, which renders the event listeners useless. 当这些路径不能成为指针事件的目标时,就没有任何事件可能会扩展到您的组,这会使事件侦听器无用。

    If a listener was executed the first time on an element ( why this can happen is explained below, so bear with me for the moment), this problem resolves itself by setting the fill property on the path, whereby making it a legitimate target for pointer events. 如果侦听器是第一次在某个元素上执行的( 为什么会发生这种情况,下面对此进行解释,所以请耐心等待),此问题将通过在路径上设置fill属性来解决,从而使其成为指针的合法目标事件。 This is why the handlers will continue functioning when they have first come to life. 这就是为什么处理程序在初次出现时将继续运行的原因。

    Side note : This effect is so powerful, that it will even influence the way the dev tools deal with these elements in Chrome and Firefox. 旁注 :此效果非常强大,甚至会影响开发工具在Chrome和Firefox中处理这些元素的方式。 When you try to inspect an element, that has fill set to none, by right clicking on it, the dev tools will open up referencing the root <svg> element instead of the element you clicked on, because the latter was not the event's target. 尝试检查填充设置为无的元素时,通过右键单击它,开发工具将打开引用根<svg>元素而不是您单击的元素,因为后者不是事件的目标。 Try this, in contrast, with an element where the event handler is already working, so to speak, and it will open the dev tools for exactly this very element. 相比之下,可以尝试使用事件处理程序已经在工作的元素,可以这样说,它将为该元素完全打开开发工具。

Solution

The easy solution to this is to allow for pointer events to occur on the interior, ie fill, of the paths by explicitly setting the property to all : 一种简单的解决方案是通过将属性显式设置为all ,从而允许指针事件发生在路径的内部(即,填充):

The element can only be the target of a mouse event when the pointer is over the interior (ie, fill) or the perimeter (ie, stroke) of the element. 仅当指针位于元素的内部(即填充)或外围(即笔触)上方时,该元素才能成为鼠标事件的目标。 The values of the fill , stroke and visibility attribute do not affect event processing. fillstrokevisibility属性的值不影响事件处理。

This can best be done right before registering the event handlers as in my updated JSFiddle : 最好像在我更新的JSFiddle中那样在注册事件处理程序之前立即完成此操作:

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

Why does it work sometimes and why the delays? 为什么有时会起作用,为什么会延迟呢?

The above provides a proper analysis and a working solution, but, if you give it some time to sink in, there still remains the question, why on earth the handlers appear to be registered or, at least, to be activated with such delays. 上面提供了适当的分析和可行的解决方案,但是,如果您给它一些时间沉浸其中,那么仍然存在一个问题,为什么在地面上似乎已注册处理程序,或者至少在这样的延迟下激活了这些处理程序。 Pondering even more on this, it turns out all information to understand the issue is already contained in my explanation. 我对此进行了更多的思考,事实证明,所有了解该问题的信息已包含在我的解释中。

As I said above, the <path> elements will actually be the event targets, not the groups. 如前所述, <path>元素实际上是事件目标,而不是组。 With the pointer-events property defaulting to visiblePainted they are not completely unreachable for pointer events as can be seen re-reading above mentioned specification: pointer-events属性默认设置为visiblePainted ,对于指针事件,它们并非完全不可访问,可以从上述规范的重新阅读中看出:

[…] or when the mouse cursor is over the perimeter (ie, 'stroke') of the element and the stroke attribute is set to a value other than none. […]或当鼠标光标悬停在元素的外围(即“笔触”)并且笔触属性设置为除none之外的值时。

Altough the infamous class st8 sets stroke: ff0000 (which obviously is other than none), it specifies stroke-width:0.24 which is a pretty thin line. 尽管臭名昭著的类st8设置了stroke: ff0000 (显然不是),但它指定了stroke-width:0.24这是一条很细的线。 Additionally being dashed, it turns out, it is hard to hit the line at all. 事实证明,如果加上破折号,根本很难达到目标。 If you actually do hit it, though, it will cause the path to become an event target with the event bubbling to the group, eventually executing the event handler. 但是,如果您确实点击了它,它将导致该路径成为事件目标,并且事件冒泡到该组,最终执行事件处理程序。 This effect can be demonstrated by setting the stroke-width to a larger value making it easier to hit the path: 可以通过将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 */
}

Have a look at this JSFiddle for a working demo. 看看这个JSFiddle上的工作演示。

Even without setting pointer-events: all this will work, because the lines are now wide enough to be hit by the pointer. 即使没有设置pointer-events: all这些都将起作用,因为行现在已经足够宽,可以被指针击中。 Because the fat lines are ugly and will break the fine layout, this is more of a demonstration than a real solution, though. 由于粗线很难看并且会破坏精细的布局,因此,这更多的是演示,而不是实际的解决方案。

This is a very interesting problem, I managed to make it work, but I have no explanation to why this works. 这是一个非常有趣的问题,我设法使它起作用,但是我没有解释为什么它起作用。 Would appreciate if someone with in-depth knowledge would explain this. 如果有深入了解的人可以解释这一点,将不胜感激。

Slow: 慢:

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");
});

Fast: 快速:

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");
});

The difference is only in applying fill to the elements before I attach event handlers to them - .style("fill", "white").on("mouseover", 区别仅在于在将事件处理程序附加到元素之前将填充应用于元素.style("fill", "white").on("mouseover",

The Fiddle to play around - https://jsfiddle.net/v8e4hnff/1/ 小提琴打转转- https://jsfiddle.net/v8e4hnff/1/

NOTE: Also tried to implement with JS native selectors and event handler attachment on the SVG elements, that was very little faster than D3. 注意:还尝试用SVG元素上的JS本机选择器和事件处理程序附件实现,这比D3快得多。 Behavior is the same on IE11 and Chrome. 行为在IE11和Chrome上相同。

As said above, if someone can explain the behavior, please do! 如上所述,如果有人可以解释该行为,请这样做!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM