事件委托实现
手写一个通用的事件委托函数
问题
实现一个通用的事件委托函数,支持在父元素上监听子元素的事件。
解答
基础实现
/**
* 事件委托函数
* @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开始向上查找,直到找到匹配元素或到达父元素 - 动态元素:委托的优势是后添加的子元素也能被监听,无需重新绑定
- 性能优化:减少事件监听器数量,适合大量同类元素的场景
目录