导读
JavaScript 中的事件处理机制是前端开发中至关重要的一部分。在复杂的网页应用中,通常需要为多个元素绑定事件监听器,这会导致代码繁杂、性能下降。事件代理(Event Delegation)是一种高效的事件处理技术,可以帮助简化代码、提高性能。
什么是事件代理?
事件代理是一种利用事件冒泡机制的技术。 它通过为一个父级元素绑定事件监听器,从而处理子元素的事件,而不是为每个子元素单独绑定事件监听器。这种方式减少了需要绑定的事件监听器数量,优化了内存使用,并且可以动态处理新添加的子元素的事件。
事件冒泡(Event Bubbling)
事件代理技术得以实现,主要是借助了事件冒泡事件流机制。因此,要完全理解事件委托的工作原理,必须要先了解事件冒泡。
事件冒泡的事件流最先是由微软在其开发的 IE 浏览器中实现的。事件冒泡的事件流,事件的触发会从最底层的 DOM 元素开始发生,一直向上传播,直到 document 对象。就像把一颗石头投入水中,泡泡会一直从水底冒出水面。事件冒泡也正因此而得名。

另外,与事件冒泡事件流相对的还有一个事件捕获事件流,它的事件触发过程与事件冒泡正好相反,如上图所示。在起初阶段,除了 IE 浏览器默认的事件流是使用的事件冒泡,其它的浏览器(Netscape)采用的是捕获,后又改为了先捕获,后冒泡。
不过经过开发人员 30 多年不断的实践,事件冒泡事件流得到开发者的青睐。目前主要的浏览器厂商已经把默认的事件流改为了事件冒泡了。来看看下面这个例子:
<div class="container" id="container">
<span id="text">Text</span>
</div>
const $container = document.querySelector('#container')
const $text = document.querySelector('#text')
const containerHandler = function(evt) {
console.log('target is container')
}
const textHandler = function(){
console.log('target is text')
}
// 目前所有的主流浏览器都将 addEventListener 方法的第三个参数设置了 fale
// 也就是使用事件冒泡了,所以必须手动设置为 true 才会执行事件捕获,包括
// onclick 这样的事件绑定方法,默认也是采用事件冒泡了
$container.addEventListener('click', containerHandler, true)
$text.addEventListener('click', textHandler, true)
示例中就使用了事件捕获,用户点击 target is text 文本,事件捕获会先触发在 body 获得 div 节点上的事件侦听器。控制台会先输出:target is container,然后才触发 text 节点的事件处理器,显示:target is text。
很明显,比起事件捕获,事件冒泡的事件触发机制更符合普通用户的实践操作的预期。例如我鼠标点击的是上图中的 text 的文本节点,通常我们更希望最先触发的是绑定在 text 节点的事件侦听器,
事件代理的原理
事件代理的核心原理是:通过在祖先元素(通常是某个容器元素)上绑定一个事件监听器,这个监听器可以捕捉到所有子元素的事件。当事件发生时,通过 event.target 属性可以确定哪个子元素触发了事件,并根据具体情况执行相应的操作。
事件代理的优势
- 减少内存占用:通过减少绑定的事件监听器数量,可以显著降低内存占用。
- 简化代码:无需为每个子元素分别添加事件监听器,代码更加简洁明了。
- 动态元素处理:新添加的子元素无需额外绑定事件监听器,自动继承父元素的事件处理逻辑
事件代理的应用场景
事件代理的主要应用场景有 列表项点击处理 和 表单输入验证,下面我们分别举例说明。
列表项点击处理
举个例子来看看事件代理的应用场景,看看它是如何减少了内存消耗,从而优化程序性能的?假设有一个 UL 带有多个子元素的父元素,其 HTML 代码如下:
<div class="toolbar">
<a id="add" class="button add" href="#add">添加</a>
</div>
<ul id="list" class="list">
<li class="item">
<p class="text">如何判断触发当前事件的 DOM 元素?</p>
<a class="delete" href="#delete">删除</a>
</li>
<li class="item">
<p class="text">参考 jQuery 的 API 实现</p>
<a class="delete" href="#delete">删除</a>
</li>
<li class="item">
<p class="text">Element.matches() 方法</p>
<a class="delete" href="#delete">删除</a>
</li>
<li class="item">
<p class="text">closest() 方法获得与选择器匹配的元素</p>
<a class="delete" href="#delete">删除</a>
</li>
<li class="item">
<p class="text">实现 on() 方法</p>
<a class="delete" href="#delete">删除</a>
</li>
<li class="item">
<p class="text">实现 off() 方法</p>
<a class="delete" href="#delete">删除</a>
</li>
</ul>
我们可以看到,这个是一个文章章节列表,在文章章节列表的最上方可以点击添加按钮新增文章章节,然后每个章节都支持删除操作。
通常的做法,可以为每个章节的删除按钮添加一个单独的事件侦听器,其 JavaScript 代码如下:
const $add = document.querySelector('#add')
const $list = document.querySelector('#list')
const $buttons = $list.querySelectorAll('.delete')
// 删除
const remove = function(evt) {
// 获取点击的删除按钮
const $target = evt.target
// 获取文章章节节点
const $li = $target.parentNode
// 删除菜单
$list.removeChild($li)
}
// 遍历所有删除按钮
$buttons.forEach($button => {
// 为每个删除按钮添加 click 事件处理器 remove 以删除章节
$button.addEventListener('click', remove)
})
目前来看一切都没有问题,但是如果添加上动态添加的书单的功能:
const add = function() {
const $li = document.createElement('li')
$li.innerHTML = `<p class="text">JavaScript 事件代理:高效事件处理的利器</p>
<a class="delete" href="#delete">删除</a>`
$list.appendChild($li)
}
$add.addEventListener('click', add)
这时会发现,点击新增按钮确实可以在章节列表中添加新的章节,但是点击删除按钮就没有任何反应了。因为我们的之前获取的 $buttons 中是不包含动态添加的章节 DOM 元素信息的。当然我们也可以这么处理一下:
const add = function() {
const $li = document.createElement('li')
let $button = null
$li.innerHTML = `<p class="text">JavaScript 事件代理:高效事件处理的利器</p>
<a class="delete" href="#delete">删除</a>`
$list.appendChild($li)
// 手动为每个新添加的 delete 按钮绑定事件处理器
$button = $li.querySelector('.delete')
$button.addEventListener('click', remove)
}
$add.addEventListener('click', add)
但是,当添加和删除代码位于应用程序中的不同位置时,这时候添加和删除事件侦听器将是一场噩梦。并且随着章节数量的不断增加,绑定的事件处理器也会越来越多,会逐渐占用很多的系统资源。更严重的问题是,如果同时还频繁进行删除的操作,按照示例代码的处理方式,没有在删除 DOM 元素前销毁 DOM 元素绑定的事件处理器,在一些浏览器中会产生内存溢出的问题。
所以像这种场景:父元素是固定的,而其中的子元素会动态增加或者删除。这个时候就适合使用事件代理,为父元素(UL)添加事件侦听器,通过事件冒泡事件流机制,父元素可以通过 event.target 监测分析出子元素的匹配项。实现只绑定一次事件处理器,可以处理其下所有子元素的事件监听。减少了内存消耗,从而优化程序性能。
表单输入验证
事件代理也可以用于表单验证。例如,一个表单中包含多个输入框,每个输入框需要在失去焦点时进行验证。可以通过事件代理,在表单的父元素上绑定一次事件监听器来实现:
const form = document.querySelector('form');
// 使用事件捕获
form.addEventListener('blur', function(event) {
const target = event.target;
if (target.tagName === 'INPUT') {
validateInput(target);
}
}, true)
function validateInput(input) {
if (input.value.trim() === '') {
input.classList.add('error');
} else {
input.classList.remove('error');
}
}
在这个例子中,blur 事件使用捕获阶段(通过传递 true 作为第三个参数)进行处理,因为 blur 事件不会冒泡。通过这种方式,所有输入框的失去焦点事件都通过一个监听器处理。
事件代理的局限性
尽管事件代理非常强大,但它并非适用于所有场景。比如:
- 事件不会冒泡:某些事件(如
focus和blur)不会冒泡,因此需要特别处理。 - 事件频繁触发:对于如
mousemove、scroll等高频率触发的事件,使用事件代理可能不太合适,因为它会导致性能下降。
不支持冒泡的事件
表单的应用场景中提到了不支持事件冒泡的机制的事件,以下是所有特殊的不支持冒泡的事件:
[
// 不会冒泡
'focusout',
'blur',
'focusin',
'focus',
'load',
'unload',
'mouseenter',
'mouseleave'
]
addEventListener() 方法的第三个参数
前文的示例代码中,使用了 addEventListener() 方法为 DOM 元素绑定事件处理器,addEventListener() 方法的第三个参数指定是使用事件冒泡还是事件捕获机制。
现在简单介绍一下 addEventListener() 方法,其基础语法如下:
addEventListener(event, function, useCapture)
参数说明:
- event - 绑定的事件名称;
- function - 绑定的事件处理器函数;
- useCapture - 是否使用捕获(默认值:false);
如果你还想体验一把事件捕获,可以将 addEventListener 的第三个参数设置为 true,就像前文的示例代码那样。还有就是针对不支持冒泡机制的事件,也需要使用事件捕获。
事件代理的 API 实现
由于事件代理是将事件侦听器添加到父级,这样一来,如何知道单击了哪个子元素成为了要解决的最大问题。
如何判断触发当前事件的 DOM 元素?
判断触发当前事件的 DOM 元素的处理方式其实很简单,面前我已经提及过了。当点击 UL 元素下的任何子元素,当事件冒泡到 UL 元素时,通过检查事件对象的 target 属性就可以获得对实际单击的子节点。简单的实现如下:
<ul id="list" class="list">
<li class="item">
<span class="book">JavaScript DOM 编程艺术</span>
<span class="delete">删除</span>
</li>
<li class="item">
<span class="book">JavaScript 高级程序设计</span>
<span class="delete">删除</span>
</li>
</ul>
const $list = document.querySelector('#list')
// 获取元素,添加点击监听器...
$list.addEventListener('click', function (e) {
// e.target 是被点击的元素!
const $delete = e.target
// 如果它是一个删除按钮
if ($delete && $delete.className === 'delete') {
console.log(`点击.delete删除按钮`);
}
})
事件代理之所以能够正常工作,最重要的原因就是事件冒泡事件流机制。点击 UL 下的子元素,由于事件冒泡,在 UL 元素上的 click 事件侦听器也会被触发。而这时,我们可以通过 event.target 获取到点击的目标元素。再通过对这个元素的一系列的判断检测是否为我们期望的元素,如果是就执行相关的操作。
参考 jQuery 的 API 实现
事件代理 API 接口实现的比较好的 JavaScript 框架应该是 jQuery 了。我们就先看看 jQuery 的事件代理的接口是怎样的:
const handler = function(evt){
console.log(`list ${$li.id} 被点击了`);
}
// 绑定事件委托处理器
$('#list').on('click', '.item', handler)
// 取消事件委托处理器绑定
$('#list').off('click', '.item', handler)
jQuery 的中提供了 on() 和 off() 两个方法,分别是绑定事件委托和取消事件委托绑定。jQuery 的实现方式比前文介绍的实现方式更加灵活,它的 on() 方法可以通过选择器(例如:.item)来分析查找子元素的匹配项。
如果想要实现和 jQuery 类似的事件代理接口,关键是需要找到一种方法判断 event.target 是否包含或者说匹配使用的选择器。如果选择器单纯的只是使用类选择器,我们可以通过 event.target.classList 属性,判断 classList 中是否包使用含使用的选择器。但是 jQuery 的 on() 方法的接口中使用的选择器是很灵活的,可以是类选择器,也可以是元素选择器,也可以是其它的选择器。为每个可能的选择器做不同的判断逻辑,那将是一个无比痛苦的事情。
Element.matches() 方法
Element.matches() 这个方法为判断 DOM 元素是否与给定的选择器匹配提供了非常便捷的方式。Element.matches() 的调用方式如下:
const isMatched = element.matches(selectorString);
如果元素被指定的选择器字符串选择,Element.matches() 方法返回 true,否则返回 false。回归到前文的示例,如果想判断 event.target 是否与 .item 选择器匹配,就可以这么调用:
const $li = event.target
if($li && $li.matches('.item')) {
console.log(`list ${$li.id} 被点击了`);
}
Element.matches() 除了调用十分方便外,各大浏览器的支持情况也很不错。

如果想兼容更多浏览器,也可以使用 MDN 给出的 polyfill,代码如下:
/**
* A polyfill for Element.matches()
* ========================================================================
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
*/
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function (selector) {
let matches = (this.document || this.ownerDocument).querySelectorAll(selector)
let i = matches.length
while (--i >= 0 && matches.item(i) !== this) {
}
return i > -1
}
}
我们再处理一下,封装成 matches() 方法:
/**
* 获取 options 节点下匹配 selector 选择器的 DOM 节点
* ========================================================================
* Element.matches() 方法可以用来判断 DOM 元素是否与给定的选择器匹配,事件代理判断是
* 否触发绑定的代理事件回调函数,关键就是使用 Element.matches() 辨别当前事件触发的目
* 标 DOM 元素是否为事件代理所期望触发的目标。
* ========================================================================
* @method matches
* @see https://developer.mozilla.org/en-US/docs/web/api/element/matches
* @param {HTMLElement} el - (必须)DOM 元素
* @param {String} selector - (必须)匹配 DOM 元素的选择器
* @returns {Boolean}
*/
const matches = (el, selector = '') => {
const sel = selector.replace(/^>/i, '')
if (!selector || !sel || !el) {
return false
}
/* istanbul ignore else */
if (el.matches) {
return el.matches(sel)
} else if (el.msMatchesSelector) {
return el.msMatchesSelector(sel)
} else {
return false
}
}
export default matches
实现 resolveTextNode() 方法
某些浏览器(例如:Safari 浏览器)会返回实际的目标元素内部的文本节点。resolveTextNode() 方法则会返回点击实际的文本节点的 Element 类型的父节点。
/**
* 在某些情况下,某些浏览器(例如:Safari 浏览器)会返回实际的目标元素内部的
* 文本节点。resolveTextNode() 方法则会返回实际的目标节点。
* ========================================================================
* @method resolveTextNode
* @param {HTMLElement|Text} el - 要解析的节点
* @return {*|HTMLElement} - 实际的目标 DOM 节点
*/
const resolveTextNode = function (el) {
if (el && el.nodeType === 3) {
return el.parentNode || el.parentElement
}
return el
}
export default resolveTextNode
closest() 方法获得与选择器匹配的元素
使用 Element.matches() 方法判断点击的元素与选择器是否匹配不是最终的目的,使用它是为了获得与选择器匹配的 DOM 元素。前文提到过了,事件代理是利用事件冒泡事件流,在事件流逐层向上冒泡的过程中,在绑定事件侦听器的父元素上来做判断分析点击的目标是否与使用的选择器匹配。
这就有一种可能,鼠标点击的直接目标可能是我们期望元素的子元素,这时是从点击的子元素开始向上冒泡,直到达到选择器匹配的元素。那么这时要获取的目标元素就是点击的目标元素的父元素。为此还需要封装一个 closest() 方法,获得与选择器匹配的元素。
import matches from './matches'
import resolveTextNode from './resolveTextNode'
/**
* 获得与选择器匹配的元素
* ========================================================================
* @param {Element} el
* @param {String} selector
* @return {Function}
*/
const closest = (el, selector) => {
// Node.ELEMENT_NODE 1 An Element node like <p> or <div>.
// Node.ATTRIBUTE_NODE 2 An Attribute of an Element.
// Node.TEXT_NODE 3 The actual Text inside an Element or Attr.
// Node.CDATA_SECTION_NODE 4 A CDATASection, such as <!CDATA[[ … ]]>.
// Node.PROCESSING_INSTRUCTION_NODE 7 A ProcessingInstruction of an XML document, such as <?xml-stylesheet … ?>.
// Node.COMMENT_NODE 8 A Comment node, such as <!-- … -->.
// Node.DOCUMENT_NODE 9 A Document node.
// Node.DOCUMENT_TYPE_NODE 10 A DocumentType node, such as <!DOCTYPE html>.
// Node.DOCUMENT_FRAGMENT_NODE 11 A DocumentFragment node.
const DOCUMENT_NODE_TYPE = 9
// 忽略 document,因为事件冒泡最终都到了 document
while (el && el.nodeType !== DOCUMENT_NODE_TYPE) {
if (matches(el, selector)) {
return el
}
el = resolveTextNode(el)
}
}
export default closest
可以看到 closest() 方法首先会比对元素的 nodeType,直到 nodeType 变为 document 类型。然后判断元素是否于选择器匹配,如果匹配,那么就返回匹配的元素,如果不匹配,则“向上冒泡”到元素的父元素,直到找到匹配的元素或者冒泡到 document。
实现 on() 方法
在完成前面的准备工作后,现在可以正式实现类似 jQuery 的 on() 方法了。
import closest from './closest'
import CAPTURE_EVENTS from './captureEvents'
/**
* 绑定代理事件
* ========================================================================
* @param {HTMLElement} el - 绑定代理事件的 DOM 节点
* @param {String} type - 事件类型
* @param {String} selector - 触发 el 代理事件的 DOM 节点的选择器
* @param {Function} callback - 绑定事件的回调函数
* @param {Object} [context] - callback 回调函数的 this 上下文(默认值:el)
* @returns {Function}
*/
const on = (el, type, selector, callback, context) => {
const capture = CAPTURE_EVENTS.indexOf(type) > -1
const listener = function (e) {
const target = e.target
// 通过 Element.matches 方法获得点击的目标元素
const delegateTarget = closest(target, selector)
e.delegateTarget = delegateTarget
if (delegateTarget) {
callback.call(context || el, e)
}
}
callback._listener = listener
el.addEventListener(type, listener, capture)
return callback
}
export default on
仔细查看代码会发现我们封装的 on() 方法的关键是使用了一个私有的 listener() 方法将 callback 回调函数包装了一下,将获取到的目标元素赋值给 event.delegateTarget 属性。并且指定了 callback 回调函数的执行上下文。
另外一个关键措施就是给 callback 函数添加了自定义的 _listener (私有)属性,这是为 off() 销毁事件侦听方法做的准备。理论上 callback 是一个事件侦听的回调函数,但由于 JavaScipt 语言的特性,函数也是对象,而 JavaScript 中的对象是可以添加任意属性的。
最后就是对于 mouseenter 和 mouseleave 事件,我们的 on() 方法直接使用了事件捕获事件流。原因是 mouseenter 和 mouseleave 事件是不适合使用事件冒泡事件流的。
实现 off() 方法
我们再实现一个 off() 方法,用来实现取消事件委托的事件侦听的绑定。
import CAPTURE_EVENTS from './captureEvents'
/**
* 取消事件绑定
* ========================================================================
* @param {HTMLElement} el - 取消绑定(代理)事件的 DOM 节点
* @param {String} type - 事件类型
* @param {Function} callback - 绑定事件的回调函数
*/
const off = (el, type, callback) => {
const capture = CAPTURE_EVENTS.indexOf(type) > -1
let listener = null
if (callback._listener) {
listener = callback._listener
delete callback._listener
}
el.removeEventListener(type, listener, capture)
}
export default off
off() 方法的实现相比 on() 方法简单多了,将 on() 方法中的 callback._delegateListener 属性移除掉,然后再调用 removeEventListener() 方法移除事件侦听器的绑定。
实现 stop() 方法
除了 on() 和 off() 方法外,在日常的开发中我们还需要一个 stop() 方法来阻止事件冒泡。还是以前文的例子为例:
<nav class="navigation">
<ul id="list" class="list">
<li class="item">
<span class="book">JavaScript DOM 编程艺术</span>
<span class="delete">删除</span>
</li>
<li class="item">
<span class="book">JavaScript 高级程序设计</span>
<span class="delete">删除</span>
</li>
</ul>
</nav>
const $nav = document.querySelector('.navigation')
const $list = document.querySelector('#list')
$nav.addEventListener('click', function(evt) {
console.log('nav to list page')
})
on($list, 'click', '.delete', remove)
通过代码可以知道,由于 list 是 nav 元素的子节点,所以点击 list 中的删除按钮时,由于事件冒泡的机制,也会触发 nav 的事件处理器。如果不希望触发 nav 的事件处理器,我们就需要一个 stop() 方法来阻止事件冒泡,代码如下:
const stop = function (evt) {
// 阻止冒泡
evt.stopPropagation()
// 阻止元素的事件的默认行为
evt.preventDefault()
}
export default stopEvent
来看看如何使用 stop() 方法来阻止冒泡:
const $nav = document.querySelector('.navigation')
const $list = document.querySelector('#list')
// 删除书单
const remove = function(evt) {
// 获取点击的删除按钮
const $target = evt.target
// 获取到书单菜单项
const $li = $target.parentNode
// 删除菜单
$list.removeChild($li)
// 阻止事件冒泡
stop(evt)
}
$nav.addEventListener('click', function(evt) {
console.log('nav to list page')
})
on($list, 'click', '.delete', remove)
实现于 jQuery 一致的 API 接口
至此,我们已经完成实现事件委托机制的功能函数了。不过我们可以稍微调整一下已经封装好的功能函数,实现一个与 jQuery 一致的 API 接口。代码封装如下:
// CAPTURE_EVENTS 中的特殊事件,采用事件捕获模型
const CAPTURE_EVENTS = [
'focusout',
'blur',
'focusin',
'focus',
'load',
'unload',
'mouseenter',
'mouseleave'
]
/**
* 获取 options 节点下匹配 selector 选择器的 DOM 节点
* ========================================================================
* Element.matches() 方法可以用来判断 DOM 元素是否与给定的选择器匹配,事件代理判断是
* 否触发绑定的代理事件回调函数,关键就是使用 Element.matches() 辨别当前事件触发的目
* 标 DOM 元素是否为事件代理所期望触发的目标。
* ========================================================================
* @method matches
* @see https://developer.mozilla.org/en-US/docs/web/api/element/matches
* @param {HTMLElement} el - (必须)DOM 元素
* @param {String} selector - (必须)匹配 DOM 元素的选择器
* @returns {Boolean}
*/
const matches = (el, selector = '') => {
const sel = selector.replace(/^>/i, '')
if (!selector || !sel || !el) {
return false
}
/* istanbul ignore else */
if (el.matches) {
return el.matches(sel)
} else if (el.msMatchesSelector) {
return el.msMatchesSelector(sel)
} else {
return false
}
}
/**
* 在某些情况下,某些浏览器(例如:Safari 浏览器)会返回实际的目标元素内部的文本节点。
* resolveTextNode() 方法则会返回实际的目标节点。
* ========================================================================
* @method resolveTextNode
* @param {HTMLElement|Text} el - 要解析的节点
* @return {*|HTMLElement} - 实际的目标 DOM 节点
*/
const resolveTextNode = function (el) {
if (el && el.nodeType === 3) {
return el.parentNode || el.parentElement
}
return el
}
/**
* 获得与选择器匹配的元素
* ========================================================================
* @param {Element} el
* @param {String} selector
* @return {Function}
*/
const closest = (el, selector) => {
// Node.ELEMENT_NODE 1 An Element node like <p> or <div>.
// Node.ATTRIBUTE_NODE 2 An Attribute of an Element.
// Node.TEXT_NODE 3 The actual Text inside an Element or Attr.
// Node.CDATA_SECTION_NODE 4 A CDATASection, such as <!CDATA[[ … ]]>.
// Node.PROCESSING_INSTRUCTION_NODE 7 A ProcessingInstruction of an XML document, such as <?xml-stylesheet … ?>.
// Node.COMMENT_NODE 8 A Comment node, such as <!-- … -->.
// Node.DOCUMENT_NODE 9 A Document node.
// Node.DOCUMENT_TYPE_NODE 10 A DocumentType node, such as <!DOCTYPE html>.
// Node.DOCUMENT_FRAGMENT_NODE 11 A DocumentFragment node.
const DOCUMENT_NODE_TYPE = 9
// 忽略 document,因为事件冒泡最终都到了 document
while (el && el.nodeType !== DOCUMENT_NODE_TYPE) {
if (matches(el, selector)) {
return el
}
el = resolveTextNode(el)
}
}
/**
* 绑定代理事件
* ========================================================================
* @param {HTMLElement} el - 绑定代理事件的 DOM 节点
* @param {String} type - 事件类型
* @param {String} selector - 触发 el 代理事件的 DOM 节点的选择器
* @param {Function} callback - 绑定事件的回调函数
* @param {Object} [context] - callback 回调函数的 this 上下文(默认值:el)
* @returns {Function}
*/
const on = (el, type, selector, callback, context) => {
const capture = CAPTURE_EVENTS.indexOf(type) > -1
const listener = function (e) {
const target = e.target
// 通过 Element.matches 方法获得点击的目标元素
const delegateTarget = closest(target, selector)
e.delegateTarget = delegateTarget
if (delegateTarget) {
callback.call(context || el, e)
}
}
callback._listener = listener
el.addEventListener(type, listener, capture)
return callback
}
/**
* 取消事件绑定
* ========================================================================
* @param {HTMLElement} el - 取消绑定(代理)事件的 DOM 节点
* @param {String} type - 事件类型
* @param {Function} callback - 绑定事件的回调函数
*/
const off = (el, type, callback) => {
const capture = CAPTURE_EVENTS.indexOf(type) > -1
let listener = null
if (callback._listener) {
listener = callback._listener
delete callback._listener
}
el.removeEventListener(type, listener, capture)
}
const stop = function (evt) {
// 阻止冒泡
evt.stopPropagation()
// 阻止元素的事件的默认行为
evt.preventDefault()
}
const Delegate = function(selector){
this.$el = document.querySelector(selector)
return this
}
Delegate.prototype = {
constructor: Delegate,
on: function(type, selector, callback) {
on(this.$el, type, selector, callback)
return this
},
off: function(type, callback) {
off(this.$el, type, callback)
return this
},
stop(evt) {
stop(evt)
return this
}
}
const mitt = (selector) => {
return new Delegate(selector)
}
我们封装的 mitt() 模块的调用方式如下:
const $ = mitt('#list')
const handler = function(evt){
console.log(`list ${$li.id} 被点击了`);
$emit.stop(evt)
}
// 绑定事件委托处理器
$.on('click', '.item', handler)
// 取消事件委托处理器绑定
$.off('click', '.item', handler)
怎么样?是不是有点 jQuery 的味道了。
演示地址:code.juejin.cn/pen/7401856…
总结
本文介绍了关于事件代理需要掌握的主要的知识点,并且还开发出了 on()、off() 和 stop() 3个常用方法。但如果你需要有一个功能更加完善的事件代理的 JavaScript 工具库, 大家可以去看看我的 delegate.js 项目,它有更加完善的事件处理的工具方法。
在 delegate.js 库中,还有更多关于事件处理的一些知识点,例如事件的默认行为的处理方式,如何阻止监听同一事件的其他事件监听器被调用,以及如何 销毁 DOM 元素所有的事件绑定。感兴趣的朋友可以阅读一下源代码,看看这些处理方式都是如何实现的。
最后总结一下,事件代理是一种高效的事件处理方式,尤其在处理大量动态生成的元素时,具有显著的优势。通过合理使用事件代理,可以使代码更加简洁、性能更高。然而,开发者也需要根据具体场景权衡其使用,确保事件代理的合理性。