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

# 事件流和事件流机制

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

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

IE 事件流为事件冒泡, Netscape Communicator 的事件流是事件捕获流。

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

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

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

② 处于目标阶段
第二个阶段是实际的目标接收到事件,事件到达目标节点的,事件流就进入到了目标阶段。
当处在这个阶段时,没有捕获与冒泡之分,事件调用顺序会按照 addEventListener 的添加顺序决定,先添加先执行。 当事件处于目标阶段时,事件调用顺序决定于 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
);

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

// listItem 的冒泡
listItem.addEventListener(
    "click",
    (e) => {
        console.log("listItem bubbling", e.eventPhase);
    },
    false
);

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

// lists capturing 1
// listItem  bubbling 2(目标阶段按addEventListener顺序)
// listItem capturing 2
// lists bubbling 3

# 事件对象

DOM2、DOM0 事件处理中,会将一个 event 对象传进事件处理程序中。在事件处理程序中,this 始终指向 currentTarget(事件绑定的元素) ,target 则始终指向位于其事件流的目标阶段,即事件的实际目标(触发事件的元素)。如果直接将事件处理程序指定给了目标元素,则 this、currentTarget 和 target 包含相同的值。

IE 事件处理中,事件对象 event 会作为 window 对象的一个属性,通过 window.event 来获取,也可以在 attachEvent 时传进。而事件目标一般用 window.event.srcElement 来获取真正的目标 target。

只有在事件处理程序期间,event 对象才会存在,一旦事件处理程序执行完成,event 对象就会被销毁。

# 事件对象的常用方法

  • 取消预设行为 event.preventDefalut

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

<a id="link" href="www.baidu.com"> click me</a>
const $link = document.getElementById("link");

$link.addEventListener(
    "click",
    (e) => {
        e.preventDefault();
    },
    false
);
  • 取消事件传递 event.stopPropagation

在对应 DOM 节点的 handler 上使用该方法,会使事件停止传递给接下来的节点们。

event.stopPropagation不会阻止当前节点上此事件其他的监听函数被调用。如果要让当前节点上的其他回调函数也不能被调用的话,可以使用 event.stopImmediatePropagation() 方法

在上面的完整栗子的基础上,在目标阶段加入event.stopPropagation的栗子:

<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 listner1", e.eventPhase);
        e.stopPropagation();
    },
    true
);

listItem.addEventListener(
    "click",
    (e) => {
        console.log("listItem capturing", "listItem listner2", e.eventPhase);
    },
    true
);

// listItem 的冒泡
listItem.addEventListener(
    "click",
    (e) => {
        console.log("listItem bubbling", e.eventPhase);
    },
    false
);

// lists capturing 1
// listItem capturing listItemL listner1 2
// listItem capturing listItem listner2 2
// listItem bubbling 2

# 兼容 IE

// 阻止预设
event.preventDefalut()   =>   event.returnValue = false;

// 取消传递
event.stopPropagation()  =>   event.cancelBubble = true;


addEventListener(event, handler, false)     =>   attachEvent('on'+event,  handler)

removeEventListener(event, handler, false)  =>   detachEvent('on'+event,  handler)


// 获取真正的目标:event.target  =>  event.srcElement

IE 不支持事件捕获,所以只能取消事件冒泡,但 stopPropagation 可以同时取消事件捕获和冒泡。

# 事件处理程序

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

  • 作用域

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

  • 添加事件处理程序数量

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

  • 触发顺序

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

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

const EventUitl = {
    // 获取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;
        }
    },
};

# 事件代理/事件委托

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

因为冒泡机制,在点击子元素时,也会触发父元素的点击事件。那么我们就可以把点击子元素的事件要做的事情,交给最外层的父元素来做,让事件冒泡到最外层的 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");
EventUitl.addEvent(list, "click", function(event) {
    event = EventUitl.getEvent(event);
    const target = EventUitl.getTarget(event);

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

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