原文地址:
JS事件流模型
1. DOM 0级模型
也称为原始事件模型,这种方式较为简单且兼容所有浏览器,但是却将界面与逻辑耦合在一起,可维护性差。
实例
当点击id
为i3
的<div>
时,控制台会依次输出2 1 0
。
<body>
<div id="i1" onclick="console.log(0)">
<div id="i2" onclick="console.log(1)">
<div id="i3" onclick="console.log(2)"></div>
</div>
</div>
</body>
2. IE 事件模型
IE8
及之前的版本是不支持捕获事件的,IE
事件模型共有两个过程:
事件处理阶段target phase
,事件到达目标元素, 触发目标元素的监听事件。
事件冒泡阶段bubbling phase
事件从目标元素冒泡到document
,依次执行经过的节点绑定的事件。
3. DOM 2级模型
DOM2
事件模型是W3C
制定的标准模型,支持捕获型事件和冒泡型事件,调用事件的处理阶段依次为捕获、目标、冒泡。
实例
当点击id
为i3
的<div>
时,浏览器会依次弹出0 1 3 2
,addEventListener
方法的第三个参数为声明绑定的事件为捕获型还是冒泡型,默认为false
,也就是冒泡型。
<body>
<div id="i1" onclick="console.log(0)">
<div id="i2" onclick="console.log(1)">
<div id="i3" onclick="console.log(2)"></div>
</div>
</div>
<script type="text/javascript">
document.addEventListener('click',(e) => {
console.log(0);
},true)
document.getElementById("i1").addEventListener('click',(e) => {
console.log(1);
},true)
document.getElementById("i2").addEventListener('click',(e) => {
console.log(2);
})
document.getElementById("i3").addEventListener('click',(e) => {
console.log(3);
})
</script>
</body>
document
对象与i1
节点绑定的是捕获型的监听事件,i2
与i3
节点绑定的是冒泡型的事件,事件传递的顺序为:
window - document - html - body - i1 - i2 - i3 - i2 - i1 - body - html - document - window
从window
到i3
的过程为捕获阶段,依次执行了过程中绑定的事件,本例中执行了alert(0)
与alert(1)
,然后到达目标阶段的i3
,执行i3
绑定的事件alert(3)
,然后从i3
到window
的阶段为冒泡阶段,执行了绑定的alert(2)
,执行顺序即为0 1 3 2
。
注意
绑定监听事件使用的区别
在DOM0
中直接绑定函数执行时,后定义的函数会覆盖前边绑定的函数,下面这个例子只执行alert(1)
而不执行alert(0)
。click()
是一个对象事件,点击即触发onclick()
绑定的方法,onclick()
是对象的属性,将其绑定函数后即为click()
事件触发后执行的方法。
<body>
<div id="i1"></div>
<script type="text/javascript">
document.getElementById("i1").onclick = function(){
alert(0); // 被覆盖
}
document.getElementById("i1").onclick = function(){
alert(1); // 执行
}
</script>
</body>
addEventListener
可以为事件绑定多个函数,并且绑定时不需要加on
,其还可以接收第三个参数useCapture
来决定事件时绑定的捕获阶段还是冒泡阶段执行。
<script type="text/javascript">
document.getElementById("i1").addEventListener('click',(e) => {
alert(0); // 执行
})
document.getElementById("i1").addEventListener('click',(e) => {
alert(1); // 执行
})
</script>
attachEvent
可以为事件绑定多个函数,绑定时需要加on
,其只支持冒泡阶段执行,所以不存在第三个参数。
<script type="text/javascript">
document.getElementById("i1").attachEvent('onclick',function(e){
alert(0); // 执行
})
document.getElementById("i1").attachEvent('onclick',function(e){
alert(1); // 执行
})
</script>
事件捕获
事件捕获 是一种从外到内
的传播方式,以click
事件为例,其会从最外层根节向内传播到达点击的节点,为从最外层节点逐渐向内传播直到目标节点的方式。
【事件捕获过程先于事件冒泡过程】
事件冒泡及阻止
事件冒泡 是一种从内到外
的传播方式,同样以click
事件为例,事件最开始由点击的节点,然后逐渐向上传播直至最高层节点。
当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到window
,当然其传播的是事件,绑定的执行函数并不会传播,如果父级没有绑定事件函数,就算传递了事件,也不会有什么表现,但事件确实传递了。
事件冒泡的原因是事件源本身可能没有处理事件的能力,即处理事件的函数并未绑定在该事件源上。它本身并不能处理事件,所以需要将事件传播出去,从而能达到处理该事件的执行函数。
实例
当点击id
为i3
的<div>
时,浏览器会依次弹出3 2 1
,这就是事件冒泡,此正方形处于叶节点上,对其操作的事件会向上进行冒泡,直到根节点。
<body>
<div id="i1">
<div id="i2">
<div id="i3"></div>
</div>
</div>
<script type="text/javascript">
document.getElementById("i1").addEventListener('click',(e) => {
alert(1);
})
document.getElementById("i2").addEventListener('click',(e) => {
alert(2);
})
document.getElementById("i3").addEventListener('click',(e) => {
alert(3);
})
</script>
</body>
应用场景
例如我们有10
个<li>
标签,每个标签有一个uid
作为判断用户点击的区别,使用冒泡就不需要为每个<li>
绑定点击事件,可以称为事件委托。
<body>
<ul id="u1">
<li uid="0">0</li>
<li uid="1">1</li>
<li uid="2">2</li>
<li uid="3">3</li>
<li uid="4">4</li>
<li uid="5">5</li>
<li uid="6">6</li>
<li uid="7">7</li>
<li uid="8">8</li>
<li uid="9">9</li>
</ul>
<script type="text/javascript">
document.getElementById("u1").addEventListener('click',(e) => {
alert(e.srcElement.getAttribute('uid'));
})
</script>
</body>
阻止冒泡
有时候我们并不希望事件冒泡而去执行上级节点绑定的事件,这时候就需要阻止事件的冒泡,w3c
的方法是e.stopPropagation()
,IE
则是使用 window.event.cancelBubble = true;
。
如果使用 vue
框架,则是@click.stop=""
注意
- 不是所有的事件都能冒泡。以下事件不冒泡:
blur
、focus
、load
、unload
。 - 事件解决方案方式在不同浏览器,可能是有所区别的,有些不支持捕获型方案,多数浏览器默认冒泡型方案。
- 阻止冒泡并不能阻止对象默认行为,例如
submit
按钮被点击后会提交表单数据,需使用e.preventDefault();
阻止默认行为,IE
则是window.event.returnValue = false;
。
事件委托
为什么要用事件委托:
比如我们有100个 li,每个 li 都有相同的 click 点击事件,可能我们会用 for 循环的方法,来遍历所有的li,然后给它们添加事件,这么做的话:会造成访问 dom 的次数很多,引起浏览器重绘与重排的次数也就很多,就会延长整个页面的交互就绪时间;
如果要用事件委托,就会将所有的操作放到 js 程序里面,与 dom 的操作就只需要交互一次,这样就能大大的减少与 dom 的交互次数,提高性能;
如果去监听每一个 li 的点击事件,就需要很多个变量来存 dom 对象,100 个 li 就需要 100 个变量对象,会造成很大的内存空间损耗。所以我们想着把利用事件冒泡由内到外的特点进行事件委托,只给所有 li 元素的父级元素 ul 添加点击事件,进行大大减少了内存空间的需求
事件委托的原理
给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件
事件委托的实现
一个列表,要求实现功能是点击不同的 item 项,作出不同的操作
<ul>
<li>hello 1</li>
<li>hello 2</li>
<li>hello 3</li>
<li>hello 4</li>
</ul>
- 没有使用事件委托
let arrLi = document.querySelectorAll('li')
for(let i = 0;i < aLi.length;i++){
aLi[i].addEventListener('click',function(e){
console.log(e.target) // li
console.log(e.currentTarget) // li
console.og(e.target === e.currentTarget) // true
})
}
- 使用事件委托
let ul = document.querySelectorAll('ul')[0]
ul.addEventListener('click',function(e){
console.log(e.target) // 被点击的li
console.log(e.currentTarget) // ul
console.log(e.target === e.currentTarget) // false
})
- e.target 指向
触发
事件监听的对象。 - e.currentTarget 指向
添加
监听事件的对象 点击 黄色区域 和 橙色区域,分别打印:
所以给父级元素 ul 添加监听事件,监听事件对象 event 的 target 属性就可以拿到被点击的 li 节点
关于 e.target与e.currentTarget的区别 在这里
现在讲的都是document加载完成的现有dom节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?就好比,一个新员工来了,他能收到快递吗?
需求场景:1. 移入li,li变红,移出li,li变白,给节点添加事件实现;2. 点击按钮,可以向ul中添加一个li子节点
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
<input type="button" name="" id="btn" value="添加" />
- 没有使用事件委托
window.onload = function () {
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName("li");
var num = 4;
function mHover() {
for (var i = 0; i < aLi.length; i++) {
aLi[i].onmouseover = function () {
this.style.background = "red";
};
aLi[i].onmouseout = function () {
this.style.background = "#fff";
};
}
}
mHover(); // 给当前的每个节点添加事件,在没有点击新增按钮的前提下还有
oBtn.onclick = function () { // 添加新节点
num++;
var oLi = document.createElement("li");
oLi.innerHTML = 111 * num;
oUl.appendChild(oLi);
mHover(); // 再次给当前的每个节点添加事件
};
};
缺点:无疑是又增加了一个 dom 操作,在优化性能方面是不可取的
- 使用事件委托
window.onload = function () {
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName("li");
var num = 4;
// 事件委托,后面添加的子元素也有事件
oUl.onmouseover = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == "li") {
target.style.background = "red";
}
};
oUl.onmouseout = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == "li") {
target.style.background = "#fff";
}
};
// 添加新节点,无需给新节点添加事件
oBtn.onclick = function () {
num++;
var oLi = document.createElement("li");
oLi.innerHTML = 111 * num;
oUl.appendChild(oLi);
};
};