# 👉 事件流和事件处理程序
# 事件流和事件流机制
事件就是用户或者浏览器自身执行的动作,比如 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 可能会指向不同的元素。
# 事件对象的常用方法
阻止默认行为
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 );
- 常见的应用场景:
停止事件传播
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冒泡依然会输出,这个可能需要注意一下
更彻底的阻止事件传播
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;
}
},
};