香草JavaScript事件委托

Vanilla JavaScript Event Delegation

本文关键字:事件 JavaScript 香草      更新时间:2023-09-26

在香草js中做事件委托的最佳方式(最快/适当)是什么?

例如,如果我在jQuery中这样写:

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

我如何将其转换为香草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();
}

与其改变内置的原型(这会导致脆弱的代码,并且经常会破坏东西),不如检查点击的元素是否有一个与你想要的选择器匹配的.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.closest('.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>

我已经提出了一个简单的解决方案似乎工作得相当好(尽管传统的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));

我有一个类似的解决方案来实现事件委托。它使用了数组函数slice, reverse, filterforEach

  • 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){...})。

委托事件

事件委托在需要执行一个函数时使用,当存在的或动态的元素(将来添加到DOM中)接收到一个事件。
该策略是将事件侦听器分配给已知的静态父节点,并遵循以下规则:

  • 使用evt.target.closest(".dynamic")获取所需的动态子节点
  • 使用evt.currentTarget获取 #staticParent 父节点委托节点
  • 使用evt.target获得确切的单击元素(警告!)这也可以是一个后代元素,不一定是.dynamic那个)

片段示例:

document.querySelector("#staticParent").addEventListener("click", (evt) => {
  const elChild = evt.target.closest(".dynamic");
  if ( !elChild ) return; // do nothing.
  console.log("Do something with elChild Element here");
});

完整的动态元素示例:

// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);

// Delegated events
el("#staticParent").addEventListener("click", (evt) => {
  const elDelegator = evt.currentTarget;
  const elChild = evt.target.closest(".dynamicChild");
  const elTarget = evt.target;
  console.clear();
  console.log(`Clicked:
    currentTarget: ${elDelegator.tagName}
    target.closest: ${elChild?.tagName}
    target: ${elTarget.tagName}`)
  if (!elChild) return; // Do nothing.
  // Else, .dynamicChild is clicked! Do something:
  console.log("Yey! .dynamicChild is clicked!")
});
// Insert child element dynamically
setTimeout(() => {
  el("#staticParent").append(elNew("article", {
    className: "dynamicChild",
    innerHTML: `Click here!!! I'm added dynamically! <span>Some child icon</span>`
  }))
}, 1500);
#staticParent {
  border: 1px solid #aaa;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.dynamicChild {
  background: #eee;
  padding: 1rem;
}
.dynamicChild span {
  background: gold;
  padding: 0.5rem;
}
<section id="staticParent">Click here or...</section>

直接事件

或者,您可以在创建时直接在子对象上附加一个click处理程序:

// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Create new comment with Direct events:
const newComment = (text) => elNew("article", {
  className: "dynamicChild",
  title: "Click me!",
  textContent: text,
  onclick() {
    console.log(`Clicked: ${this.textContent}`);
  },
});
// 
el("#add").addEventListener("click", () => {
  el("#staticParent").append(newComment(Date.now()))
});
#staticParent {
  border: 1px solid #aaa;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.dynamicChild {
  background: #eee;
  padding: 0.5rem;
}
<section id="staticParent"></section>
<button type="button" id="add">Add new</button>

资源:

  • Event.target
  • Element.closest ()
  • jQuery vs JavaScript