原生 JavaScript 事件委托

IT技术 javascript
2021-02-01 03:08:17

在 vanilla js 中进行事件委托的最佳方式(最快/正确)是什么?

例如,如果我在 jQuery 中有这个:

$('#main').on('click', '.focused', function(){
    settingsPanel();
});

我怎样才能把它翻译成 vanilla js?也许与.addEventListener()

我能想到的方法是:

document.getElementById('main').addEventListener('click', dothis);
function dothis(){
    // now in jQuery
    $(this).children().each(function(){
         if($(this).is('.focused') settingsPanel();
    }); 
 }

但这似乎效率低下,特别是如果#main有很多孩子。

这是正确的方法吗?

document.getElementById('main').addEventListener('click', doThis);
function doThis(event){
    if($(event.target).is('.focused') || $(event.target).parents().is('.focused') settingsPanel();
}
3个回答

我想出了一个简单的解决方案,它似乎工作得很好(尽管有旧版 IE 支持)。在这里,我们扩展了EventTarget的原型以提供一种delegateEventListener使用以下语法工作方法:

EventTarget.delegateEventListener(string event, string toFind, function fn)

我创建了一个相当复杂的小提琴来演示它的实际操作,我们将所有事件委托给绿色元素。停止传播继续工作,您可以访问应该是event.currentTarget通过的内容this(与 jQuery 一样)。

这是完整的解决方案:

(function(document, EventTarget) {
  var elementProto = window.Element.prototype,
      matchesFn = elementProto.matches;

  /* Check various vendor-prefixed versions of Element.matches */
  if(!matchesFn) {
    ['webkit', 'ms', 'moz'].some(function(prefix) {
      var prefixedFn = prefix + 'MatchesSelector';
      if(elementProto.hasOwnProperty(prefixedFn)) {
        matchesFn = elementProto[prefixedFn];
        return true;
      }
    });
  }

  /* Traverse DOM from event target up to parent, searching for selector */
  function passedThrough(event, selector, stopAt) {
    var currentNode = event.target;

    while(true) {
      if(matchesFn.call(currentNode, selector)) {
        return currentNode;
      }
      else if(currentNode != stopAt && currentNode != document.body) {
        currentNode = currentNode.parentNode;
      }
      else {
        return false;
      }
    }
  }

  /* Extend the EventTarget prototype to add a delegateEventListener() event */
  EventTarget.prototype.delegateEventListener = function(eName, toFind, fn) {
    this.addEventListener(eName, function(event) {
      var found = passedThrough(event, toFind, event.currentTarget);

      if(found) {
        // Execute the callback with the context set to the found element
        // jQuery goes way further, it even has it's own event object
        fn.call(found, event);
      }
    });
  };

}(window.document, window.EventTarget || window.Element));
@gilly3 我也应该计算一次,而不是每次都计算。我会更新
2021-03-21 03:08:17
您应该首先检查标准的无前缀版本。
2021-03-28 03:08:17
我建议使用window.Node而不是window.Element. 文档本身继承自Node,获得在 IE 中工作的功能(没有EventTarget)。
2021-04-10 03:08:17

与其改变内置原型(这会导致代码脆弱并且经常会破坏事物),只需检查单击的元素是否具有.closest与您想要的选择器匹配的元素。如果是,请调用您要调用的函数。例如,要翻译

$('#main').on('click', '.focused', function(){
    settingsPanel();
});

在 jQuery 之外,使用:

document.querySelector('#main').addEventListener('click', (e) => {
  if (e.target.closest('#main .focused')) {
    settingsPanel();
  }
});

除非内部选择器也可能作为父元素存在(这可能很不寻常),否则将内部选择器单独传递给.closest(例如,.closest('.focused')就足够了

使用这种模式时,为了保持紧凑,我经常将代码的主要部分放在提前返回的下方,例如:

document.querySelector('#main').addEventListener('click', (e) => {
  if (!e.target.matches('.focused')) {
    return;
  }
  // code of settingsPanel here, if it isn't too long
});

现场演示:

document.querySelector如果 DOM 尚未加载,则不会飞这不完全是授权。
2021-03-22 03:08:17
当然是 - 就像 OP 的 jQuery 代码将$('#main').on('click', '.focused', function(){点击内部委托#main#main的点击侦听器一样,我的代码也在做同样的事情。这是使用 if#main的子项是动态的(就像 if#main是一个待办事项列表容器)的技术。如果#main它本身可能不存在,那是另一种情况,当然需要将事件委托给外部父级。
2021-04-09 03:08:17
我的错……确实 OP 的情况会奏效。我应该详细说明,因为我正在寻找与Turbolinks兼容的解决方案它会删除除文档和窗口之外的所有内容。我最终只.matches使用了完整的选择器,而没有使用querySelector上一个示例中的文档。
2021-04-14 03:08:17

我有一个类似的解决方案来实现事件委托。它使用的阵列的功能slicereversefilterforEach

  • slice 将查询中的 NodeList 转换为数组,这必须在允许反转列表之前完成。
  • reverse 反转数组(使最终的遍历开始尽可能靠近事件目标。
  • filter检查哪些元素包含event.target.
  • forEach只要处理程序不返回 ,就为过滤后的结果中的每个元素调用提供的处理程序false

该函数返回创建的委托函数,这使得以后可以删除侦听器。请注意,本机event.stopPropagation()不会停止遍历validElements,因为冒泡阶段已经遍历到委托元素。

function delegateEventListener(element, eventType, childSelector, handler) {
    function delegate(event){
        var bubble;
        var validElements=[].slice.call(this.querySelectorAll(childSelector)).reverse().filter(function(matchedElement){
            return matchedElement.contains(event.target);
        });
        validElements.forEach(function(validElement){
            if(bubble===undefined||bubble!==false)bubble=handler.call(validElement,event);
        });
    }
    element.addEventListener(eventType,delegate);
    return delegate;
}

虽然不建议扩展原生原型,但可以将这个功能添加到原型中EventTarget(或Node在 IE 中)。这样做时,在函数内替换elementthis并删除相应的参数 ( EventTarget.prototype.delegateEventListener = function(eventType, childSelector, handler){...})。