【问题标题】:Vanilla JavaScript Event DelegationVanilla JavaScript 事件委托
【发布时间】:2014-05-07 03:30:42
【问题描述】:

在 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();
}

【问题讨论】:

  • event.target开始,看是否匹配选择器,如果匹配,调用函数。然后遍历它的祖先并做同样的事情,直到你到达this 元素。您可以使用node.matchesSelector() 进行测试,尽管在某些浏览器中它是使用特定于浏览器的标志实现的,因此您要么需要将其包装在一个函数中,要么将标志方法放在Element.prototype.matchesSelector
  • 建议您将其发布为答案。
  • @EvanTrimboli:我不喜欢发布答案。我会让别人得到代表。如果您愿意,请随意。
  • @cookiemonster 为什么我会找到子元素然后上去?
  • @MrGuru:因为这就是事件委托的工作方式。您必须查看元素层次结构中是否有任何节点与选择器匹配,从事件发起的元素开始,到您绑定处理程序的元素停止。

标签: javascript


【解决方案1】:

I've come up with a simple solution 似乎工作得很好(尽管有旧版 IE 支持)。在这里,我们扩展EventTarget 的原型以提供delegateEventListener 方法,该方法使用以下语法:

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

我创建了一个相当复杂的 fiddle 来演示它的实际效果,我们将所有事件委托给绿色元素。停止传播继续起作用,您可以通过 this 访问应该是 event.currentTarget 的内容(与 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 我也应该计算一次,而不是每次。我会更新
  • 我建议使用window.Node 而不是window.Element。文档本身继承自 Node,获得在 IE 中工作的功能(没有 EventTarget)。
【解决方案2】:

不要改变内置原型(这会导致代码脆弱并且经常破坏事物),只需检查单击的元素是否具有与您想要的选择器匹配的 .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('#outer').addEventListener('click', (e) => {
  if (!e.target.closest('#inner')) {
    return;
  }
  console.log('vanilla');
});

$('#outer').on('click', '#inner', () => {
  console.log('jQuery');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="outer">
  <div id="inner">
    inner
    <div id="nested">
      nested
    </div>
  </div>
</div>

【讨论】:

  • document.querySelector is not going to fly 如果 DOM 尚未加载。这不完全是委托。
  • 当然是——就像 OP 的 $('#main').on('click', '.focused', function(){ 的 jQuery 代码将 #main 内的点击委托给 #main 的点击监听器一样,我的代码也在做同样的事情。如果#main 的孩子是动态的(比如#main 是一个待办事项列表容器),这就是使用的技术。如果#main 本身可能不存在,那是另一种情况,当然需要将事件委托给外部父级。
  • 我的错.. 确实 OP 的情况会奏效。我应该在寻找与Turbolinks 兼容的解决方案时进行详细说明。它会删除除文档和窗口之外的所有内容。我最终只使用了带有完整选择器的.matches,而在文档中没有querySelector,就像你上一个例子一样。
【解决方案3】:

我有一个类似的解决方案来实现事件委托。 它利用数组函数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(或者IE中的Node)的原型中添加这个功能。这样做时,将函数内的element替换为this,并删除对应的参数(EventTarget.prototype.delegateEventListener = function(eventType, childSelector, handler){...})。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-01-18
    • 1970-01-01
    • 2019-03-10
    • 1970-01-01
    • 2021-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多