如果我遵循正常的D3方式,我是否将eventListener添加到每个SVG元素?

时间:2018-01-03 14:44:01

标签: javascript d3.js svg dom-events

在d3中处理onclick的正常方法是

selection.append(element)
    .on("click", someFunction)

如果我在1000 svg元素上这样做,是否意味着我只附加了1000个不同的听众。如果是这种情况,是否有专门针对d3的事件委托?

3 个答案:

答案 0 :(得分:6)

@AlexW answer(部分)正确:D3中没有事件委托,只有事件绑定。

但是,我说部分因为最好说“在D3”中没有用于事件委派的原生方法,因为实际上它很容易实现它:使用D3进行事件委托的丑陋的替代方法包括使用d3.event.target

例如,在这个非常简单的演示中,我们绑定了这些数据......

var data = ["foo", "bar", "baz"];

...到<g>元素内的圈子。然后,我们将一个事件监听器绑定到该组,并在单击时获取每个圆的基准:

g.on("click", function() {
  console.log(d3.select(d3.event.target).datum())
})

就是这样:

var svg = d3.select("svg");
var g = svg.append("g");
var data = ["foo", "bar", "baz"];
var circles = g.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("cy", 40)
  .attr("cx", function(_, i) {
    return 50 + 100 * i
  })
  .attr("r", 20)
  .attr("fill", "teal");

g.on("click", function() {
  console.log(d3.select(d3.event.target).datum())
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

这种方法的好处在于,就像jQuery事件委托一样,它适用于在定义了监听器之后创建的元素。在以下演示中,红色圆圈:

var svg = d3.select("svg");
var g = svg.append("g");
var data = ["foo", "bar", "baz"];
var circles = g.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("cy", 40)
  .attr("cx", function(_, i) {
    return 50 + 75 * i
  })
  .attr("r", 20)
  .attr("fill", "teal");

g.on("click", function() {
  console.log(d3.select(d3.event.target).datum())
});

g.append("circle")
  .attr("cy", 40)
  .attr("cx", 275)
  .attr("r", 20)
  .attr("fill", "firebrick")
  .datum("foobar")
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

因此,尽管D3没有本地的,明确的事件委派方法,但解决方案非常简单明了。

答案 1 :(得分:3)

是的,这会添加1000个事件监听器:

  

为指定的每个选定元素添加或删除侦听器   事件类型名称。

https://github.com/d3/d3-selection/blob/master/README.md#selection_on

如果你有1000多个元素,你可能不想使用SVG,因为DOM很容易陷入这么多元素的困境。使用画布等可能更有效。

D3不进行事件委托,只进行事件绑定。因此,如果您仍在考虑使用SVG,则可能需要实施委派using jQuery or vanilla JS

答案 2 :(得分:0)

这是d3事件委托的实现:

/**
 * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
 *
 * @param { String } selector A string containing a selector expression to match elements against.
 *
 * @return { Array } D3 selection

 * */
d3.selection.prototype.closest = function(selector) {
  let closestMatchDom = undefined
  let matchArr = []
  this.each(function() {
    let currentDom = this
    while(typeof currentDom.parentNode.matches === 'function' && !closestMatchDom) { // from itself
      if(currentDom.matches(selector)) {
        closestMatchDom = currentDom
        matchArr.push(closestMatchDom)
      }
      currentDom = currentDom.parentNode
    }
    closestMatchDom = undefined
  })
  return d3.selectAll(matchArr)
}

/**
 * DIY D3 Event Delegation
 *
 * @param { String } parentSelector
 * @param { String } childSelector
 * @param { String } events Such as 'click contextmenu'
 * @param { PlainObject } data A custom data which is passed to handler.
 * @param { Function } handler A function to execute when the event of childDom is triggered.
 * @param { Function } inverseHandler A function to execute when the event of childDom is not triggered but the event of parentDom is triggered
 *
 * */
function eventDelegate(parentSelector, childSelector, events, data, handler, inverseHandler) {
  let $container = d3.select(parentSelector)
  $container.on(events, function () {
    let event = d3.event
    let target = event.target
    let $target = d3.select(target)
    let $currentTarget = null

    if (!$target.closest(childSelector).empty() && !$container.select(childSelector).empty()) {
      $currentTarget = $target.closest(childSelector)
    } else {
      if (inverseHandler) {
        inverseHandler.call(target, $target, data)
      }
      return
    }
    d3.event.data = data
    handler.call($currentTarget.node(), $currentTarget, data)
  })
}