js事件流
什么事件流
事件流是事件源于DOM实现并传递到文档对象模型的过程。事件捕获和事件冒泡的方法,以及各种事件侦听器注册技术,允许以多种方式处理事件。它可以在EventTarget级别本地处理,也可以从文档树中更高级别的EventTarget集中处理。
在DOM2级事件模型中规定事件流包括三个阶段: (1)事件捕获阶段 (2)处于目标阶段 (3)事件冒泡阶段
image.pngDOM事件处理
DOM节点中有了事件,那我们就需要对事件进行处理,而DOM事件处理分为4个级别:DOM0级事件处理,DOM0级事件处理,DOM2级事件处理和DOM3级事件处理。
image.png
其中DOM1级事件处理标准中并没有定义相关的内容,所以没有所谓的DOM1事件处理;DOM3级事件在DOM2级事件的基础上添加了更多的事件类型。
DOM0
DOM0级事件具有极好的跨浏览器优势,会以最快的速度绑定。第一种方式是内联模型(行内绑定),将函数名直接作为html标签中属性的属性值。
<div onclick="btnClick()">click</div>
<script>
function btnClick(){
console.log("hello");
}
</script>
内联模型的缺点是不符合w3c中关于内容与行为分离的基本规范。第二种方式是脚本模型(动态绑定),通过在JS中选中某个节点,然后给节点添加onclick属性。
<div id="btn">点击</div>
<script>
var btn=document.getElementById("btn");
btn.onclick=function(){
console.log("hello");
}
</script>
点击输出hello,没有问题;如果我们给元素添加两个事件
<div id="btn">点击</div>
<script>
var btn=document.getElementById("btn");
btn.onclick=function(){
console.log("hello");
}
btn.onclick=function(){
console.log("hello again");
}
</script>
这时候只有输出hello again,很明显,第一个事件函数被第二个事件函数给覆盖掉,所以脚本模型的缺点是同一个节点只能添加一次同类型事件。让我们把div扩展到3个。
<div id="btn3">
btn3
<div id="btn2">
btn2
<div id="btn1">
btn1
</div>
</div>
</div>
<script>
let btn1 = document.getElementById("btn1");
let btn2 = document.getElementById("btn2");
let btn3 = document.getElementById("btn3");
btn1.onclick=function(){
console.log(1)
}
btn2.onclick=function(){
console.log(2)
}
btn3.onclick=function(){
console.log(3)
}
</script>
当我们点击btn3的时候输出3,那当我们点击btn1的时候呢?
image.png我们发现最先触发的是最底层btn1的事件,最后才是顶层btn3的事件,因此很明显是事件冒泡。DOM0级只支持冒泡阶段
image.pngDOM2
进一步规范之后,有了DOM2级事件处理程序,其中定义了两个方法:
addEventListener() ---添加事件侦听器
removeEventListener() ---删除事件侦听器
函数均有3个参数, 第一个参数是要处理的事件名 第二个参数是作为事件处理程序的函数 第三个参数是一个boolean值,默认false表示使用冒泡机制,true表示捕获机制。
<div id="btn">点击</div>
<script>
var btn=document.getElementById("btn");
btn.addEventListener("click",hello,false);
btn.addEventListener("click",helloagain,false);
function hello(){
console.log("hello");
}
function helloagain(){
console.log("hello again");
}
</script>
这时候两个事件处理程序都能够成功触发,说明可以绑定多个事件处理程序,但是注意,如果定义了一摸一样时监听方法,是会发生覆盖的,即同样的事件和事件流机制下相同方法只会触发一次,
<div id="btn">点击</div>
<script>
var btn=document.getElementById("btn");
btn.addEventListener("click",hello,false);
btn.addEventListener("click",hello,false);
function hello(){
console.log("hello");
}
</script>
这时候hello只会执行一次;让我们把div扩展到3个。
<div id="btn3">
btn3
<div id="btn2">
btn2
<div id="btn1">
btn1
</div>
</div>
</div>
<script>
let btn1 = document.getElementById('btn1');
let btn2 = document.getElementById('btn2');
let btn3 = document.getElementById('btn3');
btn1.addEventListener('click',function(){
console.log(1)
}, true)
btn2.addEventListener('click',function(){
console.log(2)
}, true)
btn3.addEventListener('click',function(){
console.log(3)
}, true)
</script>
image.png
这时候看到顺序和DOM0中的顺序反过来了,最外层的btn最先触发,因为addEventListener最后一个参数是true,捕获阶段进行处理。
image.png那么冒泡和捕获阶段谁先执行呢?我们给每个元素分别绑定了冒泡和捕获两个事件。
btn1.addEventListener('click',function(){
console.log('btn1捕获')
}, true)
btn1.addEventListener('click',function(){
console.log('btn1冒泡')
}, false)
btn2.addEventListener('click',function(){
console.log('btn2捕获')
}, true)
btn2.addEventListener('click',function(){
console.log('btn2冒泡')
}, false)
btn3.addEventListener('click',function(){
console.log('btn3捕获')
}, true)
btn3.addEventListener('click',function(){
console.log('btn2冒泡')
}, false)
image.png
我们看到先执行捕获阶段的处理程序,后执行冒泡阶段的处理程序,我们把顺序换一下再看运行结果:
btn1.addEventListener('click',function(){
console.log('btn1冒泡')
}, false)
btn1.addEventListener('click',function(){
console.log('btn1捕获')
}, true)
btn2.addEventListener('click',function(){
console.log('btn2冒泡')
}, false)
btn2.addEventListener('click',function(){
console.log('btn2捕获')
}, true)
btn3.addEventListener('click',function(){
console.log('btn3冒泡')
}, false)
btn3.addEventListener('click',function(){
console.log('btn3捕获')
}, true)
image.png
我们发现在触发的目标元素上不区分冒泡还是捕获,按绑定的顺序来执行。
阻止事件进一步传播
有时候我们需要点击事件不再继续向上冒泡,我们在btn2上加上stopPropagation函数,阻止程序冒泡。
btn1.addEventListener('click',function(){
console.log('btn1冒泡')
}, false)
btn1.addEventListener('click',function(){
console.log('btn1捕获')
}, true)
btn2.addEventListener('click',function(){
console.log('btn2冒泡')
}, false)
btn2.addEventListener('click',function(ev){
ev.stopPropagation();
console.log('btn2捕获')
}, true)
btn3.addEventListener('click',function(){
console.log('btn3冒泡')
}, false)
btn3.addEventListener('click',function(e){
console.log('btn3捕获')
}, true)
image.png
可以看到btn2捕获阶段执行后不再继续往下执行。
image.png事件委托
如果有多个DOM节点需要监听事件的情况下,给每个DOM绑定监听函数,会极大的影响页面的性能,因为我们通过事件委托来进行优化,事件委托利用的就是冒泡的原理。
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
var li_list = document.getElementsByTagName('li')
for(let index = 0;index<li_list.length;index++){
li_list[index].addEventListener('click', function(ev){
console.log(ev.currentTarget.innerHTML)
})
}
</script>
正常情况我们给每一个li都会绑定一个事件,但是如果这时候li是动态渲染的,数据又特别大的时候,每次渲染后(有新增的情况)我们还需要重新来绑定,又繁琐又耗性能;这时候我们可以将绑定事件委托到li的父级元素,即ul。
var ul_dom = document.getElementsByTagName('ul')
ul_dom[0].addEventListener('click', function(ev){
console.log(ev.target.innerHTML)
})
上面代码中我们使用了两种获取目标元素的方式,target和currentTarget,那么他们有什么区别呢:
- target返回触发事件的元素,不一定是绑定事件的元素
- currentTarget返回的是绑定事件的元素
因此我们总结一下事件委托的优点:
- 提高性能:每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少。
- 动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以一样具有和其他元素一样的事件。