咱们知道,若是给 form 里面的 button 元素绑定事件,须要考虑它是否会触发 form 的 submit 行为。除此以外,其它场合给 button 元素绑定事件,你几乎不用担忧这个事件会有什么非预期的附加效果,很天然地会这样写事件处理代码:css
var button = document.querySelector('button') button.addEventListener('click', function (e) { console.log('点击了按钮') })
你之因此放心这么写,是由于这个 button 元素没有使用事件代理,即没有代理任何子元素的事件。html
事件代理的意思是,你要为一个元素绑定事件,但你不是直接把事件绑定到这个元素本身身上,而是绑定到这个元素的父元素上。当子元素的某个事件(好比点击事件)触发时,它的父元素相同的事件也会触发(咱们常说的事件冒泡),此时咱们说父元素代理了子元素的事件。svg
举个例子,好比一个 button 元素中包含一个齿轮图标:3d
<button> <svg> <use xlink:href="#gear"></use> </svg> </button>
当用户点击齿轮图标,必然要触发 click 事件,但你并不会直接绑定事件到 svg 或 use 元素上,而是绑定到它们的父元素 button 上。即:代理
document.querySelector('button').addEventListener('click', function (e) { console.log('点击了按钮') })
这种状况,咱们能够说,button 元素代理了它的全部子元素的 click 事件。code
可是,出现这种事件代理的状况时,咱们就得当心了。orm
为了更直观地说明问题,咱们把“父”元素上升到顶层的 document 元素:htm
document.documentElement.addEventListener('click', function (e) { console.log('我被点击了') })
只要网页中任意一个位置被点击了,都会触发绑定在 document 元素上的点击事件。 想要知道事件具体是发生在哪一个元素上面,能够经过事件对象提供的 target 属性来判断。对象
document.documentElement.addEventListener('click', function (e) { console.log(e.target) })
咱们很容易知道事件具体是发生在哪一个元素身上的。因而在上面的示例中,若是父元素 document 想在按钮被点击时作点什么事情,咱们很天然地会这么写:blog
document.documentElement.addEventListener('click', function (e) { if (e.target.tagName === 'BUTTON') { console.log('按钮被点击了') } })
这时问题就出现了,按钮即便被点击了 if 条件也不必定成立,即也不必定会输出“按钮被点击了”。由于用户在按钮上的某个位置点击了,根据用户点击的位置,e.target 多是下面三种状况:
实际的状况是这样的:
咱们真正的意图是,只要点击是发生在按钮上面,不管是按钮的哪一个位置,咱们都应视为按钮被点击了。 嗯,简单,咱们再改一下,这样写:
document.documentElement.addEventListener('click', function (e) { if (['BUTTON', 'SVG', 'USE'].includes(e.target.tagName.toUpperCase())) { // 点击的是按钮 } })
这样彷佛没什么问题,也确实能够达到目的,但看上去老是有些别扭。由于这种状况对于最上层的 document 来讲,得知道每一个子元素的状况,原本我只须要关心离我最近的 button 元素就能够了。
根据 OOP 对内封装的思想,button 元素内部的事情应该在内部消化掉,其子元素对外不可见,应该只暴露 button 元素自己。依据这个思想和事件冒泡的特色,咱们就有了比较好的解决办法:只须要禁止 button 内部元素的事件响应(包括事件冒泡)而只容许 button 元素自己的事件发生就行。有两种方式能够实现这个目的。
一种是使用 CSS 禁止 button 内部元素的事件响应:
button > * { pointer-events: none; }
另外一种是使用 JS 来阻止 button 内部元素的事件响应(包括事件冒泡):
document.querySelector('button > svg').addEventListener('click', function (e) { e.stopPropagation() e.preventDefault() }) document.querySelector('button').addEventListener('click', function (e) { console.log(e.target.tagName) })
这两种方式都能达到咱们预期的效果:
综上,针对特定元素进行事件处理时,若是该元素有事件代理的状况,就要当心处理它所代理的子元素。