捕获和冒泡使我们能够实现一种称为事件委托的最强大的事件处理模式之一。
其思想是,如果我们有很多以类似方式处理的元素,那么我们不会为每个元素分配一个处理程序,而是为它们的公共祖先放置一个处理程序。
在处理程序中,我们获取 event.target
来查看事件实际发生的位置并处理它。
让我们看一个示例 - 八卦图 反映了古代中国哲学。
它在这里
HTML 如下所示
<
table
>
<
tr
>
<
th
colspan
=
"
3"
>
<
em
>
Bagua</
em
>
Chart: Direction, Element, Color, Meaning</
th
>
</
tr
>
<
tr
>
<
td
class
=
"
nw"
>
<
strong
>
Northwest</
strong
>
<
br
>
Metal<
br
>
Silver<
br
>
Elders</
td
>
<
td
class
=
"
n"
>
...</
td
>
<
td
class
=
"
ne"
>
...</
td
>
</
tr
>
<
tr
>
...2 more lines of this kind...</
tr
>
<
tr
>
...2 more lines of this kind...</
tr
>
</
table
>
表格有 9 个单元格,但可以有 99 个或 9999 个,这并不重要。
我们的任务是单击时突出显示一个单元格 <td>
。
我们不会为每个 <td>
(可能很多)分配一个 onclick
处理程序 - 我们将在 <table>
元素上设置“catch-all”处理程序。
它将使用 event.target
来获取单击的元素并突出显示它。
代码
let
selectedTd;
table.
onclick
=
function
(
event
)
{
let
target =
event.
target;
// where was the click?
if
(
target.
tagName !=
'TD'
)
return
;
// not on TD? Then we're not interested
highlight
(
target)
;
// highlight it
}
;
function
highlight
(
td
)
{
if
(
selectedTd)
{
// remove the existing highlight if any
selectedTd.
classList.
remove
(
'highlight'
)
;
}
selectedTd =
td;
selectedTd.
classList.
add
(
'highlight'
)
;
// highlight the new td
}
这样的代码不关心表格中有多少个单元格。我们可以随时动态添加/删除 <td>
,突出显示仍然有效。
不过,有一个缺点。
单击可能不会发生在 <td>
上,而是在其内部。
在我们的案例中,如果我们查看 HTML 内部,我们可以看到 <td>
内部的嵌套标签,例如 <strong>
<
td
>
<
strong
>
Northwest</
strong
>
...
</
td
>
自然地,如果单击发生在该 <strong>
上,那么它将成为 event.target
的值。
在处理程序 table.onclick
中,我们应该获取这样的 event.target
并找出单击是否在 <td>
内部。
以下是改进后的代码
table.
onclick
=
function
(
event
)
{
let
td =
event.
target.
closest
(
'td'
)
;
// (1)
if
(
!
td)
return
;
// (2)
if
(
!
table.
contains
(
td)
)
return
;
// (3)
highlight
(
td)
;
// (4)
}
;
解释
- 方法
elem.closest(selector)
返回与选择器匹配的最近祖先。在我们的案例中,我们从源元素向上查找<td>
。 - 如果
event.target
不在任何<td>
内,则调用会立即返回,因为没有事情可做。 - 在嵌套表格的情况下,
event.target
可能是一个<td>
,但位于当前表格之外。因此,我们检查它是否实际上是我们的表格的<td>
。 - 如果是,则突出显示它。
结果,我们有了一个快速、高效的突出显示代码,它不关心表格中 <td>
的总数。
委托示例:标记中的操作
事件委托还有其他用途。
假设我们要制作一个带有按钮“保存”、“加载”、“搜索”等的菜单。并且有一个带有方法 save
、load
、search
…的对象,如何匹配它们?
第一个想法可能是为每个按钮分配一个单独的处理程序。但有一个更优雅的解决方案。我们可以为整个菜单添加一个处理程序,并为具有要调用的方法的按钮添加 data-action
属性
<
button
data-action
=
"
save"
>
Click to Save</
button
>
处理程序读取属性并执行方法。看看工作示例
<
div
id
=
"
menu"
>
<
button
data-action
=
"
save"
>
Save</
button
>
<
button
data-action
=
"
load"
>
Load</
button
>
<
button
data-action
=
"
search"
>
Search</
button
>
</
div
>
<
script
>
class
Menu
{
constructor
(
elem
)
{
this
.
_elem =
elem;
elem.
onclick =
this
.
onClick
.
bind
(
this
)
;
// (*)
}
save
(
)
{
alert
(
'saving'
)
;
}
load
(
)
{
alert
(
'loading'
)
;
}
search
(
)
{
alert
(
'searching'
)
;
}
onClick
(
event
)
{
let
action =
event.
target.
dataset.
action;
if
(
action)
{
this
[
action]
(
)
;
}
}
;
}
new
Menu
(
menu)
;
</
script
>
请注意,this.onClick
在 (*)
中绑定到 this
。这很重要,因为否则其中的 this
将引用 DOM 元素(elem
),而不是 Menu
对象,并且 this[action]
将不是我们需要的。
那么,委托在这里给我们带来了什么好处?
- 我们不需要编写代码为每个按钮分配一个处理程序。只需创建一个方法并将其放入标记中。
- HTML 结构是灵活的,我们可以随时添加/删除按钮。
我们还可以使用类 .action-save
、.action-load
,但属性 data-action
在语义上更好。我们还可以在 CSS 规则中使用它。
“行为”模式
我们还可以使用事件委托以声明方式向元素添加“行为”,使用特殊属性和类。
该模式有两部分
- 我们向一个元素添加一个自定义属性来描述其行为。
- 一个文档范围的处理程序跟踪事件,如果事件发生在带属性的元素上——执行该操作。
行为:计数器
例如,这里属性 data-counter
为按钮添加了一个行为:“点击时增加值”
Counter: <
input
type
=
"
button"
value
=
"
1"
data-counter
>
One more counter: <
input
type
=
"
button"
value
=
"
2"
data-counter
>
<
script
>
document.
addEventListener
(
'click'
,
function
(
event
)
{
if
(
event.
target.
dataset.
counter !=
undefined
)
{
// if the attribute exists...
event.
target.
value++
;
}
}
)
;
</
script
>
如果我们点击一个按钮——它的值就会增加。不是按钮,但这里重要的是通用方法。
可以有任意多个带有 data-counter
的属性。我们可以随时向 HTML 中添加新的属性。使用事件委托,我们“扩展”了 HTML,添加了一个描述新行为的属性。
addEventListener
当我们为 document
对象分配一个事件处理程序时,我们应该始终使用 addEventListener
,而不是 document.on<event>
,因为后者会导致冲突:新处理程序会覆盖旧处理程序。
对于实际项目来说,由代码的不同部分设置的 document
上有很多处理程序是正常的。
行为:切换器
另一个行为示例。点击具有属性 data-toggle-id
的元素将显示/隐藏具有给定 id
的元素
<
button
data-toggle-id
=
"
subscribe-mail"
>
Show the subscription form
</
button
>
<
form
id
=
"
subscribe-mail"
hidden
>
Your mail: <
input
type
=
"
email"
>
</
form
>
<
script
>
document.
addEventListener
(
'click'
,
function
(
event
)
{
let
id =
event.
target.
dataset.
toggleId;
if
(
!
id)
return
;
let
elem =
document.
getElementById
(
id)
;
elem.
hidden =
!
elem.
hidden;
}
)
;
</
script
>
让我们再次注意我们所做的。现在,要向元素添加切换功能——不需要了解 JavaScript,只需使用属性 data-toggle-id
。
这可能会变得非常方便——无需为每个此类元素编写 JavaScript。只需使用该行为。文档级别的处理程序使其适用于页面的任何元素。
我们还可以在单个元素上组合多个行为。
“行为”模式可以作为 JavaScript 微片段的替代方案。
摘要
事件委托非常酷!这是用于 DOM 事件的最有用的模式之一。
它通常用于为许多类似的元素添加相同的处理,但不仅限于此。
算法
- 在容器上放置一个单独的处理程序。
- 在处理程序中 - 检查源元素
event.target
。 - 如果事件发生在我们感兴趣的元素内部,则处理该事件。
优点
- 简化初始化并节省内存:无需添加许多处理程序。
- 代码更少:添加或删除元素时,无需添加/删除处理程序。
- DOM 修改:我们可以使用
innerHTML
等批量添加/删除元素。
当然,委托有其局限性
- 首先,事件必须是冒泡的。某些事件不会冒泡。此外,低级处理程序不应使用
event.stopPropagation()
。 - 其次,委托可能会增加 CPU 负载,因为容器级处理程序会对容器中任何地方的事件做出反应,无论我们是否感兴趣。但通常负载可以忽略不计,因此我们不考虑它。