# 👉 事件流和事件处理程序
# 事件流和事件流机制
事件就是用户或者浏览器自身执行的动作,比如 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;
}
});
大多数情况下,事件委托都会将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。利用事件冒泡机制托管事件处理程序来提高程序性能。