# 👉 事件流和事件处理程序

# 事件流和事件流机制

事件就是用户或者浏览器自身执行的动作,比如 click、mouseover。

事件流则是用来描述网页元素接受事件的传递顺序,事件流机制主要有两种:事件捕获(从上向下)事件冒泡(从下向上)

DOM2 级事件规定的事件流则包括三个阶段: 事件捕获 -> 目标 -> 事件冒泡

// PhaseType event.eventPhase
const unsigned short      CAPTURING_PHASE                = 1;
const unsigned short      AT_TARGET                      = 2;
const unsigned short      BUBBLING_PHASE                 = 3;

① 事件捕获阶段
首先发生的是事件捕获,在这个阶段中,事件开始从 window 对象开始往下逐级传播, 直到 TARGET(所点击的目标)。

② 处于目标阶段
第二个阶段是实际的目标接收到事件,事件到达目标节点的,事件流就进入到了目标阶段。
目标阶段仍然遵循事件流规则(捕获 -> 目标 -> 冒泡),不会按照 addEventListener 书写顺序执行,而是按照是否捕获或冒泡的顺序执行。

③ 事件冒泡阶段
最后一个阶段是冒泡阶段,事件会从目标逆向回流,往上逐级传递回最外层节点。

但并非所有的事件都会经过冒泡阶段的,所有的事件都要经过捕捉阶段和目标阶段,但有些事件会跳过冒泡阶段。例如,让元素获得输入焦点的 focus 事件以及失去输入焦点的 blur 事件就都不会冒泡。

<ul id="lists">
    <li id="list-item">click me!</li>
</ul>
const get = (id) => document.getElementById(id);
const lists = get("lists");
const listItem = get("list-item");

// list 的捕获
lists.addEventListener(
    "click",
    (e) => {
        console.log("lists capturing", e.eventPhase);
    },
    true // true 表示捕获阶段监听
);

// list 的冒泡
lists.addEventListener(
    "click",
    (e) => {
        console.log("lists bubbling", e.eventPhase);
    },
    false // false 表示冒泡阶段监听
);

// listItem 的冒泡
listItem.addEventListener(
    "click",
    (e) => {
        console.log("listItem bubbling", e.eventPhase);
    },
    false // 第三属性useCapture,默认为false,false表示在事件冒泡阶段调用事件处理函数
);

// listItem 的捕获
listItem.addEventListener(
    "click",
    (e) => {
        console.log("listItem capturing", e.eventPhase);
    },
    true
);

// lists capturing 1
// listItem capturing 2
// listItem  bubbling 2(忽略addEventListener添加顺序,会按先捕获再冒泡的顺序
// lists bubbling 3

# 事件对象

在事件流的事件处理中,将一个 event 对象传进事件处理程序中:

1.event.target

  • 始终指向其事件流目标阶段的元素,即触发事件的实际目标元素。
  • 不会随着事件流的传播而变化,无论事件处于捕获阶段、目标阶段、冒泡阶段,event.target 都是事件最初触发的元素。

2.event.currentTarget

  • 指向当前正在执行事件处理程序的元素,即绑定了该事件监听的元素。
  • 会随着事件流的传播而变化,在不同的事件传播阶段(捕获、目标、冒泡),event.currentTarget 可能会指向不同的元素。

# 事件对象的常用方法

  1. 阻止默认行为 event.preventDefault

    • 常见的应用场景:
      • 阻止 <a> 标签的默认跳转
      • 阻止 <form> 提交
      • 阻止右键菜单(contextmenu 事件)。

    比如点击 a 标签会链接至其 href 属性对应的 url,要取消这个预设行为可以用到 event.preventDefault

    <a id="link" href="www.baidu.com"> click me</a>
    
    const $link = document.getElementById("link");
    
    $link.addEventListener(
        "click",
        (e) => {
            e.preventDefault();
        },
        false
    );
    
  2. 停止事件传播 event.stopPropagation

    阻止事件继续向父或子元素传播。如果添加在元素的捕获阶段上,则会阻断该元素的冒泡及该元素向上或向下继续传播事件。

    如果要让当前节点上的其他回调函数也不能被调用的话,可以使用 event.stopImmediatePropagation() 方法。

    • 常见的应用场景:只想触发某个元素的事件,而不触发其父级事件。
    <ul id="lists">
        <li id="list-item">click me!</li>
    </ul>
    
    const get = (id) => document.getElementById(id);
    const lists = get("lists");
    const listItem = get("list-item");
    
    // lists 的捕获
    lists.addEventListener(
        "click",
        (e) => {
            console.log("lists capturing", e.eventPhase);
        },
        true
    );
    
    // lists 的冒泡
    lists.addEventListener(
        "click",
        (e) => {
            console.log("lists bubbling", e.eventPhase);
        },
        false
    );
    
    // listItem 的捕获
    listItem.addEventListener(
        "click",
        (e) => {
            console.log("listItem capturing", "listItem listener1", e.eventPhase);
            e.stopPropagation();
        },
        true
    );
    
    listItem.addEventListener(
        "click",
        (e) => {
            console.log("listItem capturing", "listItem listener2", e.eventPhase);
        },
        true
    );
    
    // listItem 的冒泡
    listItem.addEventListener(
        "click",
        (e) => {
            console.log("listItem bubbling", e.eventPhase);
        },
        false
    );
    
    // lists capturing 1
    // listItem capturing listItemL listener1 2
    // listItem capturing listItem listener2 2
    // PS: 谷歌浏览器中listItem冒泡也不会输出,但在旧版本一些浏览器listItem冒泡依然会输出,这个可能需要注意一下
    
  3. 更彻底的阻止事件传播 event.stopImmediatePropagation()

    不仅阻止事件传播,还会阻止当前元素上其他同类型事件的监听器执行。当前元素后续监听器被跳过,事件传播也被终止。

    // lists 的捕获
    lists.addEventListener(
        "click",
        (e) => {
            console.log("lists capturing", e.eventPhase);
        },
        true
    );
    
    // lists 的冒泡
    lists.addEventListener(
        "click",
        (e) => {
            console.log("lists bubbling", e.eventPhase);
        },
        false
    );
    
    // listItem 的捕获
    listItem.addEventListener(
        "click",
        (e) => {
            console.log("listItem capturing", "listItem listener1", e.eventPhase);
            e.stopImmediatePropagation();
        },
        true
    );
    
    listItem.addEventListener(
        "click",
        (e) => {
            console.log("listItem capturing", "listItem listener2", e.eventPhase);
        },
        true
    );
    
    // listItem 的冒泡
    listItem.addEventListener(
        "click",
        (e) => {
            console.log("listItem bubbling", e.eventPhase);
        },
        false
    );
    
    // lists capturing 1
    // listItem capturing listItemL listener1 2
    

# 兼容 IE

  • 事件流的常用方法在 IE浏览器中的用法有所不同:

    // 阻止预设
    event.preventDefault()   =>   event.returnValue = false;
    
    // 取消传递
    event.stopPropagation()  =>   event.cancelBubble = true;
    
    
    ddEventListener(event, handler, false)     =>   attachEvent('on'+event,  handler);
    
    removeEventListener(event, handler, false)  =>   detachEvent('on'+event,  handler);
    
    
    // 获取真正的目标:event.target  =>  event.srcElement
    
    // IE 不支持事件捕获,所以只能取消事件冒泡,但 stopPropagation 可以同时取消事件捕获和冒泡。
    

# 事件代理/事件委托

事件委托利用了事件冒泡的原理,通过只指定一个事件处理程序,就可以达到管理某一类型的所有事件。

因为冒泡机制,在点击子元素时,也会触发父元素的点击事件。那么我们就可以把点击子元素的事件要做的事情,交给最外层的父元素来做,让事件冒泡到最外层的 dom 节点上触发事件处理程序。

例如,click 事件会一直冒泡到 document 层次,也就是说,当需要为一个有 100 个 list 的列表分别添加 click 的事件处理程序的时候,可以通过为这 100 个 list 的父节点 ul 绑定事件处理程序进而达到目的,而不必为每个 list 元素分别添加事件处理程序。

<ul id="myLinks">
    <li id="goSomewhere">Go somewhere</li>
    <li id="doSomething">Do something</li>
    <li id="sayHi">Say hi</li>
</ul>
const list = document.getElementById("myLinks");
EventUtil.addEvent(list, "click", function(event) {
    event = EventUtil.getEvent(event);
    const target = EventUtil.getTarget(event);

    switch (target.id) {
        case "doSomething":
            console.log("doSomething");
            break;
        case "goSomewhere":
            console.log("goSomewhere");
            break;
        case "sayHi":
            console.log("hi");
            break;
    }
});

大多数情况下,事件委托都会将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。利用事件冒泡机制托管事件处理程序来提高程序性能。

# 事件处理程序 (可忽略)

一共有三种事件处理程序:DOM0、DOM2、IE,他们的主要区别:

  • 作用域

    DOM0、DOM2 的 handler 会在所属元素的作用域内运行,即 this 指向当前引用的元素;IE 的 handler 会在全局作用域运行,this === window。

  • 添加事件处理程序数量

    DOM2、IE 可以添加多个事件处理程序,DOM0 只支持一个。
    DOM2 直接添加的匿名函数 handler 无法移除,addEventListener 和 removeEventListener 的 handler 必须同名

  • 触发顺序

    当添加加多个事件处理程序时,DOM2 会按照代码编写添加顺序执行,IE 会以相反的顺序执行。

# 通用的跨浏览器事件处理程序

在现代浏览器中,这一部分可以忽略,大多浏览器已统一。

const EventUtil = {
    // 获取event对象
    getEvent: (event) => {
        return event ? event : window.event;
    },

    // 获取目标
    getTarget: (event) => {
        return event.target ? event.target : event.srcElement;
    },

    // 取消默认行为
    preventDefault: (event) => {
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    },

    // 取消事件传播
    stopPropagation: (event) => {
        if (event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBubble = false;
        }
    },

    // element 是当前元素,可通过getElementById(id)获取
    // type 是事件类型,一般是click ,也有可能是鼠标、焦点、滚轮事件等等
    // handler 指定事件处理函数的时期或阶段(boolean)

    // 事件监听
    addEvent: (element, type, handler) => {
        if (element.addEventListener) {
            // 第三个参数,默认为false冒泡阶段
            element.addEventListener(type, handler, false);
            return handler;
        } else if (element.attachEvent) {
            const wrapper = function() {
                const event = event ? event : window.event;
                const target = event.target ? event.target : event.srcElement;
                handler.call(element, event);
            };
            element.attachEvent("on" + type, wrapper);
            return wrapper;
        } else {
            element[on + "click"] = handler;
        }
    },

    // 取消事件监听
    removeEvent: (element, type, handler) => {
        if (element.removeEventListener) {
            element.removeEventListener(type, handler, false);
        } else if (element.detachEvent) {
            element.detachEvent("on" + type, handler);
        } else {
            element[on + "click"] = null;
        }
    },
};