编程

JavaScript 中的事件委托(delegate)

956 2023-09-26 22:39:00

这个问题是我偶然碰到的,不是投票排名很高的问题,但我觉得还蛮重要的。

基础

Event Bubbling

要理解 JavaScript 中的 Event Delegation,首先需要了解 Event Bubbling。

之前在 event.preventDefault() vs. return false 这个问题中已经对 JavaScript 事件模型说得比较细了。其中的 Event Flow 有三个部分:

  • Capturing。事件从祖先到 EventTarget 节点。
  • Hit Target。事件到达目标节点。
  • Bubbling。事件由 EventTarget 节点到祖先。

注意这个 Bubbling,它就是 JavaScript Event Delegation 的关键。即:发生在 DOM 节点上的事件会向上传递到祖先节点。

Event Delegation

Event Bubbling 为 Event Delegation 提供了基础,它使得你可以:

把若干个节点上的相同事件的处理函数 event listener 绑定到它的父节点上去,在父节点上统一处理,减轻对 event listener 的管理负担。

这就是事件委托。通常用在一组元素上。比如 <ul> 节点下的一组 <li> 元素。

应用

举个栗子。

一个博客的首页,展示前 6 篇博客的列表。代码结构如下:

<ul id="parent-list">
    <li id="post-1">Post 1</li>
    <li id="post-2">Post 2</li>
    <li id="post-3">Post 3</li>
    <li id="post-4">Post 4</li>
    <li id="post-5">Post 5</li>
    <li id="post-6">Post 6</li>
</ul>

假如用户在点击列表中每篇文章的时候我们需要用 JavaScript 做点儿什么,比如展示一个什么效果,发起一个 ajax 请求等等。有两种做法:

  • 在每个 <li> 元素上绑定 click 事件
  • <ul> 元素上绑定 click 事件

如果你使用第一种,想象一下,万一将来你要给这个首页列表添加 lazy loading 功能,你还要逐个去处理新添加元素的 event listener。你知道,添加和移除 event listener 的工作是不好管理的,很痛苦的。

更好的方案是使用第二种:把 event listener 放在父元素上。

为什么能这么做

  • 前面提到的 Event Bubbling 保证了 <ul> 元素上的事件会传递到 <ul> 元素
  • event listener 中传入的 event 对象中包含了事件发生的真实节点—— <li> 的引用,你可以通过 event 对象访问到 <li> 节点的所有信息

如何实现

<ul> 节点上添加 event listener:

// Get the element, add a click listener...
document.getElementById("parent-list").addEventListener("click", function(e) {
    // e.target is the clicked element
    // If it was a list item
    if(e.target && e.target.nodeName == "LI") {
        // List item found.  Do whatever you want.
        console.log("List item ", e.target.id.replace("post-"), " was clicked!");
    }
});

通过这种方式,<li> 节点可以任意添加,也不用担心 event listener 的添加和移除问题了。

更广泛的应用

上面的例子是简单场景下的示范:子元素都是 <li> 类型。

其实这个 Group 的概念可以是不同元素的集合。比如:

  • 一个 <div> 下有不同类型的子元素
  • 各个子元素的 class 属性会根据场景而改变
  • 我们要处理的对象是 <tag class="classA"> 的点击事件,即带有 classA 样式的节点的点击事件

你无法对某个具体的节点去绑定 click event listener,因为你不知道到底哪个节点会满足条件。当然你还可以对所有元素绑定,但你知道这很笨。(看到重点了没:一组元素都绑定类似的 event listener。这时候,就可以考虑使用 Event Delegation)

这种情况下,使用 Event Delegation 更方便。

// Get the parent DIV, add click listener...
document.getElementById("myDiv").addEventListener("click",function(e) {
    // Get the CSS classes, e.target was the clicked element
    var classes = e.target.className.split(" ");
    if(classes) {
        for(var x = 0; x < classes.length; x++) {
            if(classes[x] == "classA") {
                // Bingo!
                console.log("Target element clicked!");
            }
        }
    }
});

以上。