一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
事件模型
W3C
中定义事件的发生经历三个阶段:捕获阶段(capturing
)—> 目标阶段(targetin
)—> 冒泡阶段(bubbling
)
DOM
事件流:同时支持两种事件模型:捕获型事件和冒泡型事件
事件冒泡还是事件捕获
事件传播是一种定义当发生事件时元素次序的方法。假如 <div>
元素内有一个 <p>
,然后用户点击了这个 <p>
元素,应该首先处理哪个元素“click”事件?
在冒泡中,最内侧元素的事件会首先被处理,然后是更外侧的:首先处理 <p>
元素的点击事件,然后是 <div>
元素的点击事件。
在捕获中,最外侧元素的事件会首先被处理,然后是更内侧的:首先处理 <div>
元素的点击事件,然后是 <p>
元素的点击事件。
在 addEventListener()
方法中,你能够通过使用“useCapture”参数来规定传播类型:
addEventListener(event, function, useCapture);
默认值是 false,将使用冒泡传播,如果该值设置为true,则事件使用捕获传播。
true
表示该元素在事件的“捕获阶段”(由外往内传递时)响应事件false
表示该元素在事件的“冒泡阶段”(由内向外传递时)响应事件
removeEventListener()
方法会删除已通过 addEventListener() 方法附加的事件处理程序:
element.removeEventListener(event, function);
冒泡
冒泡型事件:当你使用事件冒泡时,子级元素先触发,父级元素后触发
<head>
<style>
.parent{
width: 200px;
height: 200px;
background-color: antiquewhite;
}
.child{
width: 100px;height: 100px;
margin: 30px;
background-color: cadetblue;
}
</style>
</head>
<body>
<div class="parent" onclick="parent()">
父元素
<div class="child" onclick="child()">子元素</div>
</div>
<script>
function parent() {
console.log('点击父元素')
}
function child() {
console.log('点击子元素')
}
</script>
</body>
默认添加事件是冒泡模式
阻止冒泡:在W3c
中,使用stopPropagation()
方法;在IE下设置cancelBubble = true
捕获
捕获型事件:当你使用事件捕获时,父级元素先触发,子级元素后触发
<head>
<style>
.parent{
width: 200px;
height: 200px;
background-color: rgb(204, 245, 182);
}
.child{
width: 100px;height: 100px;
margin: 30px;
background-color: cadetblue;
}
</style>
</head>
<body>
<div class="parent" id="parent">
父元素
<div class="child" id="child">子元素</div>
</div>
<script>
function parent() {
console.log('点击父元素')
}
function child() {
console.log('点击子元素')
}
// addEventListener第3个参数为true是捕获,false为冒泡
document.getElementById("parent").addEventListener("click", parent, true);
document.getElementById("child").addEventListener("click", child, true);
</script>
</body>
阻止捕获:阻止事件的默认行为,例如click - <a>
后的跳转。在W3c
中,使用preventDefault()
方法,在IE
下设置window.event.returnValue = false
addEventListener的兼容
IE 8、Opera 6.0 及其更早版本不支持 addEventListener() 和 removeEventListener() 方法。不过,对于这些特殊的浏览器版本,您可以使用 attachEvent() 方法向元素添加事件处理程序,并由 detachEvent() 方法删除:
element.attachEvent(event, function);
element.detachEvent(event, function);
示例
跨浏览器解决方案:
var x = document.getElementById("myBtn");
if (x.addEventListener) { // 针对主流浏览器,除了 IE 8 及更正版本
x.addEventListener("click", myFunction);
} else if (x.attachEvent) { // 针对 IE 8 及更早版本
x.attachEvent("onclick", myFunction);
}
事件的代理/委托
事件代理(Event Delegation
),又称之为事件委托。是 JavaScript
中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。
-
原理:事件委托是指将事件绑定目标元素的到父元素上,事件代理的原理是DOM元素的事件冒泡机制触发该事件
-
优点:1. 可以减少事件注册,节省大量内存占用。2. 可以将事件应用于动态添加的子元素上,实现当新增子元素对象时无需再次对其绑定事件
-
缺点: 使用不当会造成事件在不应该触发时触发
-
常见使用场景:比如在
table
上代理所有td
的click
事件就非常棒。或是ul
中所有li
上的事件可以通过ul
来代理。
示例
<head>
<style>
li{
width: 200px;
margin: 10px 0;
background-color: cornflowerblue;
}
</style>
</head>
<body>
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<script>
var ulEl = document.getElementById("ul")
// 所有li的事件统一绑定在ul上
ulEl.addEventListener('click', function(e){
var target = e.target || e.srcElement; // 取到实际触发的target
if(!!target && target.nodeName.toUpperCase() === "LI"){
console.log(target.innerHTML);
}
}, false);
// 动态新增的li不需要额外绑定事件
var li = document.createElement('li')
li.innerText = '5'
ulEl.appendChild(li)
</script>
</body>
事件“捕获”和“冒泡”执行顺序和事件的执行次数
-
按照W3C标准的事件:首是进入捕获阶段,直到达到目标元素,再进入冒泡阶段
-
事件执行次数(addEventListener):元素上绑定事件的个数
- 注意1:前提是事件被确实触发
- 注意2:事件绑定几次就算几个事件,即使类型和功能完全一样也不会“覆盖”
-
事件执行顺序:判断的关键是否目标元素
- 非目标元素:根据W3C的标准执行:捕获->目标元素->冒泡(不依据事件绑定顺序)
- 目标元素:依据事件绑定顺序:先绑定的事件先执行(不依据捕获冒泡标准)
- 最终顺序:父元素捕获->目标元素事件1->目标元素事件2->子元素捕获->子元素冒泡->父元素冒泡
- 注意:子元素事件执行前提 事件确实“落”到子元素布局区域上,而不是简单的具有嵌套关系
<head>
<style>
.parent{
width: 200px;
height: 200px;
background-color: rgb(204, 245, 182);
}
.current{
width: 130px; height: 130px;
margin: 15px;
background-color: rgb(244, 191, 121);
}
.child{
width: 70px;height: 70px;
margin: 15px;
background-color: cadetblue;
}
</style>
</head>
<body>
<div class="parent" id="parent">
父元素
<div class="current" id="current">
目标元素
<div class="child" id="child">子元素</div>
</div>
</div>
<script>
document.getElementById("parent").addEventListener("click", function() {
console.log('parent冒泡触发')
}, false);
document.getElementById("parent").addEventListener("click", function() {
console.log('parent捕获触发')
}, true);
document.getElementById("current").addEventListener("click", function() {
console.log('目标元素事件1')
});
document.getElementById("current").addEventListener("click", function() {
console.log('目标元素事件2')
});
document.getElementById("current").addEventListener("click", function() {
console.log('目标捕获触发')
}, true);
document.getElementById("child").addEventListener("click", function() {
console.log('child冒泡触发')
}, false);
document.getElementById("child").addEventListener("click", function() {
console.log('child捕获触发')
}, true);
</script>
</body>
点击下图红点处:
在一个DOM上同时绑定两个点击事件:一个用捕获,一个用冒泡。事件会执行几次,先执行冒泡还是捕获?
- 该DOM上的事件如果被触发,会执行两次(执行次数等于绑定次数)
- 如果该DOM是目标元素,则按事件绑定顺序执行,不区分冒泡/捕获
- 如果该DOM是处于事件流中的非目标元素,则先执行捕获,后执行冒泡
W3C事件的 target 与 currentTarget 的区别?
target
只会出现在事件流的目标阶段currentTarget
可能出现在事件流的任何阶段- 当事件流处在目标阶段时,二者的指向相同
- 当事件流处于捕获或冒泡阶段时:
currentTarget
指向当前事件活动的对象(一般为父级)
如何派发事件(dispatchEvent)?(如何进行事件广播?)
- W3C: 使用
dispatchEvent
方法 - IE: 使用
fireEvent
方法
var fireEvent = function(element, event){
if (document.createEventObject){
var mockEvent = document.createEventObject();
return element.fireEvent('on' + event, mockEvent)
}else{
var mockEvent = document.createEvent('HTMLEvents');
mockEvent.initEvent(event, true, true);
return !element.dispatchEvent(mockEvent);
}
}