事件委托实现

手写一个通用的事件委托函数

问题

实现一个通用的事件委托函数,支持在父元素上监听子元素的事件。

解答

基础实现

/**
 * 事件委托函数
 * @param {Element} parent - 父元素
 * @param {string} eventType - 事件类型
 * @param {string} selector - 目标元素选择器
 * @param {Function} callback - 回调函数
 * @returns {Function} 取消监听的函数
 */
function delegate(parent, eventType, selector, callback) {
  const handler = (event) => {
    // 从触发事件的元素开始,向上查找匹配的元素
    let target = event.target;
    
    while (target && target !== parent) {
      // 检查当前元素是否匹配选择器
      if (target.matches(selector)) {
        // 调用回调,绑定 this 为匹配的元素
        callback.call(target, event, target);
        return;
      }
      target = target.parentNode;
    }
  };

  parent.addEventListener(eventType, handler);

  // 返回取消监听的函数
  return () => {
    parent.removeEventListener(eventType, handler);
  };
}

使用示例

<ul id="list">
  <li data-id="1"><span>项目 1</span></li>
  <li data-id="2"><span>项目 2</span></li>
  <li data-id="3"><span>项目 3</span></li>
</ul>
const list = document.getElementById('list');

// 委托监听所有 li 的点击事件
const cancel = delegate(list, 'click', 'li', function(event, target) {
  console.log('点击了:', target.dataset.id);
  console.log('this:', this); // 指向匹配的 li 元素
});

// 动态添加的元素也能被监听
const newItem = document.createElement('li');
newItem.dataset.id = '4';
newItem.innerHTML = '<span>项目 4</span>';
list.appendChild(newItem);

// 取消监听
// cancel();

增强版:支持多个选择器

function delegateMultiple(parent, eventType, selectorMap) {
  const handler = (event) => {
    let target = event.target;
    
    while (target && target !== parent) {
      // 遍历所有选择器
      for (const [selector, callback] of Object.entries(selectorMap)) {
        if (target.matches(selector)) {
          callback.call(target, event, target);
          return;
        }
      }
      target = target.parentNode;
    }
  };

  parent.addEventListener(eventType, handler);
  
  return () => parent.removeEventListener(eventType, handler);
}

// 使用
delegateMultiple(document.body, 'click', {
  '.btn-edit': (e, el) => console.log('编辑', el),
  '.btn-delete': (e, el) => console.log('删除', el),
  '.btn-view': (e, el) => console.log('查看', el),
});

jQuery 风格的链式调用

function $(selector) {
  const el = typeof selector === 'string' 
    ? document.querySelector(selector) 
    : selector;

  return {
    on(eventType, selectorOrCallback, callback) {
      // 如果第二个参数是函数,直接绑定事件
      if (typeof selectorOrCallback === 'function') {
        el.addEventListener(eventType, selectorOrCallback);
        return this;
      }
      
      // 否则使用事件委托
      delegate(el, eventType, selectorOrCallback, callback);
      return this;
    }
  };
}

// 使用
$('#list').on('click', 'li', function(e) {
  console.log('点击了 li');
});

关键点

  • 事件冒泡:事件从目标元素向上冒泡到父元素,委托利用这一机制在父元素统一处理
  • matches 方法:使用 element.matches(selector) 判断元素是否匹配选择器
  • 向上遍历:从 event.target 开始向上查找,直到找到匹配元素或到达父元素
  • 动态元素:委托的优势是后添加的子元素也能被监听,无需重新绑定
  • 性能优化:减少事件监听器数量,适合大量同类元素的场景