如何高效地实现 addEventListener?

时间:2021-07-20 22:03:54

标签: javascript algorithm performance user-interface events

我正在用画布开发一个简单的 2D 浏览器游戏,但我已经到了需要实现一个简单用户界面的地步。具体来说,我有一个快捷栏,用户可以点击它来选择一个项目。在游戏世界中单击也会使用您在该位置选择的项目。所以当前的问题是点击快捷栏也会导致它在游戏世界中点击。

相反,如果用户点击快捷栏(只是一个用画布绘制的矩形),我希望快捷栏处理该 mousedown 事件并使其停止向上传播到游戏画布元素,这样只有该项目被选中,并且该项目没有使用。

因此,我为我的自定义用户界面元素开发了一个类似于 addEventListener 的功能,但存在一个问题:它非常慢。

例如,mousedown 运行良好。当用户点击某个位置时,它会遍历父层次结构以找到最顶层的元素,如下所示:

getTopMostElement(x, y) {
  let root = this.rootElement;

  for (let i = 0; i < root.children.length; i++) {
    let child = root.children[i];
    let bounds = child.getBounds();

    if (bounds && rectangleContainsPoint(bounds.x, bounds.y, bounds.w, bounds.h, x, y)) {
      i = 0;
      root = child;
    }
  }

  return root;
}

然后检查该元素是否具有 mouseDown 侦听器,如果有,则调用它。如果该侦听器调用 stopPropagation,则它停止传播。否则,它会向上迭代该元素的父层次结构,依次调用每个侦听器。

问题在于 mousemove 之类的事件。这个 O(n) 操作实在是太慢了。也就是说,每次用户移动鼠标时,都要向下迭代整个元素层次结构,找到鼠标位置最顶层的元素,然后向上迭代。有没有更有效的方法来做到这一点?浏览器如何做到这一点?目前我只有 10-20 个 UI 元素,所以我认为即使是空间分区或空间散列之类的东西也不会比简单的 for 循环更有效。

1 个答案:

答案 0 :(得分:0)

公平地说,浏览器的优势在于能够 a) 运行本机代码和 b) 不必处理 JS 和本机 DOM 元素之间的绑定,例如获取边界/儿童/....

虽然我不完全确定浏览器是如何做到这一点的(尽管我相信您可以找到关于此的文档或研究论文),但我立即想到了一些想法:

  • 让侦听器类型是一个链表,其中一个处理程序指向下一个。这使您可以轻松跳过元素 A 与其后代元素 B 之间层次结构中的 30 个级别
  • 将您的画布分成多个块并跟踪哪些元素在哪些块中。这使得检查某个 X/Y 位置是否在元素的边界内更便宜/更快。例如。如果元素甚至不是块的一部分,则无需再检查其边界
  • 非常占用内存,但您可以为每个像素(或像素块)保留最顶级的元素,这使得计算起点的成本非常低(当然,这很难跟踪)< /li>

您可能会想到更多优化,因为您对实现的细节了如指掌。例如。对于鼠标单击/传播,它是否从最顶层元素开始,然后通过其祖先传播?或者它是否通过同一位置的其他元素“传播”?例如:

root           (z-index 0)
|- div A       (z-index 2)
|- div B       (z-index 1)
    |- button  (z-index 3)

想象一下,由于某种原因,您单击了一个 X/Y 位置,其中 div A、div B 和按钮都在边界内。由于按钮是最顶层呈现的元素,您的事件处理从那里开始。但是接下来会去哪里呢? button > div B > root?还是button > div A > div B > root例如传播分层或可视化。 像这样的决策会影响您的“框架”的工作方式以及优化它的方式。

相关问题