本文系统梳理了 Web 事件模型的三种绑定方式,重点讲解事件冒泡机制及事件委托的实现思路;通过 data-* 属性演示声明式行为绑定,并介绍如何使用 CustomEvent 创建和触发自定义事件,深入理解浏览器事件处理流程。
事件简介
- HTML 特性(attribute):
onclick="..."
。 - DOM 属性(property):
elem.onclick = function
。 - 方法(method):
elem.addEventListener(event, handler[, phase])
用于添加,removeEventListener
用于移除。
HTML 特性很少使用,因为 HTML 标签中的 JavaScript 看起来有些奇怪且陌生。而且也不能在里面写太多代码。
DOM 属性用起来还可以,但我们无法为特定事件分配多个处理程序。在许多场景中,这种限制并不严重。
最后一种方式是最灵活的,但也是写起来最长的。有少数事件只能使用这种方式。例如
transtionend
和
DOMContentLoaded
(上文中讲到了)。addEventListener
也支持对象作为事件处理程序。在这种情况下,如果发生事件,则会调用
handleEvent
方法。
无论你如何分类处理程序 —— 它都会将获得一个事件对象作为第一个参数。该对象包含有关所发生事件的详细信息。
冒泡
冒泡(bubbling)原理很简单。
当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
event.target
父元素上的处理程序始终可以获取事件实际发生位置的详细信息。
引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过
event.target
访问。
注意与
this
(=event.currentTarget
)之间的区别:
event.target
—— 是引发事件的“目标”元素,它在冒泡过程中不会发生变化。this
—— 是“当前”元素,其中有一个当前正在运行的处理程序。
事件委托
捕获和冒泡允许我们实现最强大的事件处理模式之一,即 事件委托 模式。
这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。
在处理程序中,我们获取 event.target
以查看事件实际发生的位置并进行处理。
该表格有 9 个单元格(cell),但可以有 99 个或 9999 个单元格,这都不重要。
我们的任务是在点击时高亮显示被点击的单元格
<td>
。
与其为每个 <td>
(可能有很多)分配一个
onclick
处理程序 —— 我们可以在 <table>
元素上设置一个“捕获所有”的处理程序。
1 | let selectedTd; |
我们还可以使用事件委托将“行为(behavior)”以 声明方式 添加到具有特殊特性(attribute)和类的元素中。
行为模式分为两个部分:
- 我们将自定义特性添加到描述其行为的元素。
- 用文档范围级的处理程序追踪事件,如果事件发生在具有特定特性的元素上 —— 则执行行为(action)。
行为:计数器
例如,这里的特性 data-counter
给按钮添加了一个“点击增加”的行为。
1 | Counter: <input type="button" value="1" data-counter> |
如果我们点击按钮 —— 它的值就会增加。但不仅仅是按钮,一般的方法在这里也很重要。
我们可以根据需要使用 data-counter
特性,多少都可以。我们可以随时向 HTML
添加新的特性。使用事件委托,我们属于对 HTML
进行了“扩展”,添加了描述新行为的特性。
行为:切换器
再举一个例子。点击一个具有 data-toggle-id
特性的元素将显示/隐藏具有给定 id
的元素:
1 | <button data-toggle-id="subscribe-mail">Show the subscription form</button> |
自定义数据属性:data-*
这种方式通过访问一个元素的 dataset 属性来存取 data-*自定义属性的值。
使用这种方法时,不是使用完整的属性名,如 data-id 来存取数据,应该去掉 data-前缀。
还有一点特别注意的是:data-属性名如果包含了连字符,例如 data-id-and-class,连字符将被去掉,并转换为驼峰式的命名,前面的属性应该写成 idAndClass。
浏览器默认行为
阻止浏览器行为
有两种方式来告诉浏览器我们不希望它执行默认行为:
- 主流的方式是使用
event
对象。有一个event.preventDefault()
方法。 - 如果处理程序是使用
on<event>
(而不是addEventListener
)分配的,那返回false
也同样有效。
1 | <a href="/" onclick="return false">Click here</a> |
创建自定义事件
事件构造器
内建事件类形成一个层次结构(hierarchy),类似于 DOM 元素类。根是内建的 Event 类。
我们可以像这样创建 Event
对象:
1 | let event = new Event(type[, options]); |
参数:
type —— 事件类型,可以是像这样
"click"
的字符串,或者我们自己的像这样"my-event"
的参数。options —— 具有两个可选属性的对象:
bubbles: true/false
—— 如果为true
,那么事件会冒泡。cancelable: true/false
—— 如果为true
,那么“默认行为”就会被阻止。稍后我们会看到对于自定义事件,它意味着什么。
默认情况下,以上两者都为 false:
{bubbles: false, cancelable: false}
。
dispatchEvent
事件对象被创建后,我们应该使用 elem.dispatchEvent(event)
调用在元素上“运行”它。
自定义事件
对于我们自己的全新事件类型,例如 "hello"
,我们应该使用
new CustomEvent
。从技术上讲,CustomEvent 和
Event
一样。除了一点不同。
在第二个参数(对象)中,我们可以为我们想要与事件一起传递的任何自定义信息添加一个附加的属性
detail
。
1 | <h1 id="elem">Hello for John!</h1> |
事件中的事件是同步的
通常事件是在队列中处理的。也就是说:如果浏览器正在处理
onclick
,这时发生了一个新的事件,例如鼠标移动了,那么它的处理程序会被排入队列,相应的
mousemove
处理程序将在 onclick
事件处理完成后被调用。
值得注意的例外情况就是,一个事件是在另一个事件中发起的。例如使用
dispatchEvent
。这类事件将会被立即处理,即在新的事件处理程序被调用之后,恢复到当前的事件处理程序。