面试中被问到JavaScript的事件流,这里就通俗的给大家解释一下事件流

81 阅读4分钟

js事件流这个东西比较枯燥抽象,所以为了以后的自己能够看懂,尽量写的通俗一点 如图有3个元素

image-20220309123153136.png

比如这种情况。给a注册一个点击事件,点击b和c也能触发。这个应该不难理解

但是如果我将c用绝对定位移走呢?

image-20220309123405725.png

这个时候点击一下c,您猜怎么着?

没错,你点击c,还是一样触发a的点击事件。这个也不难理解

疑问来了:

如果a和b和c都注册了点击事件,触发顺序是怎么样的? ``

var a = document.getElementById('wrap');
var b = document.getElementById('outer');
var c = document.getElementById('inner');

a.addEventListener('click',function(){
  alert('触发了a');
},false);
b.addEventListener('click',function(){
  alert('触发了b');
},false);
c.addEventListener('click',function(){
  alert('触发了c');
},false)

懂的同学可能知道,如果点最里面的c,就会按照 c-> b ->a 的顺序触发alert 没错,这就是传说中的冒泡事件

但是!!!来了!!!!!!!!

var a = document.getElementById('wrap');
var b = document.getElementById('outer');
var c = document.getElementById('inner');

a.addEventListener('click',function(){
  alert('触发了a');
},true);
b.addEventListener('click',function(){
  alert('触发了b');
},true);
c.addEventListener('click',function(){
  alert('触发了c');
},true)

把false改成true会怎么样?

你猜怎么样?会跟冒泡反过来。点击c,会先a->b->c。点击b,会先a->b.

这个叫在事件的捕获阶段再调用方法。

加大点难度,如果捕获和冒泡都监听了会怎么样????

var a = document.getElementById('wrap');
var b = document.getElementById('outer');
var c = document.getElementById('inner');

a.addEventListener('click',function(){
  alert('触发了a');
},false);
b.addEventListener('click',function(){
  alert('触发了b');
},false);
c.addEventListener('click',function(){
  alert('触发了c');
},false)

a.addEventListener('click',function(){
  alert('触发了a');
},true);
b.addEventListener('click',function(){
  alert('触发了b');
},true);
c.addEventListener('click',function(){
  alert('触发了c');
},true)

您猜怎么着?

JS的事件流会这样定义,会先进行捕获,然后再进行冒泡。例如你点击c,就会出现 a捕获,b捕获,c捕获,c冒泡,b冒泡,a冒泡

所以JS事件流是:最外层 -> 最里层 捕获触发, 然后最里层 -> 最外层 冒泡。这个就是一个事件流。

如果这样呢?

var a = document.getElementById('wrap');
var b = document.getElementById('outer');
var c = document.getElementById('inner');

a.addEventListener('click',function(){
  alert('触发了a');
},false);
b.addEventListener('click',function(){
  alert('触发了b');
},false);
c.addEventListener('click',function(){
  alert('触发了c');
},false)

a.addEventListener('click',function(){
  alert('触发了a');
},true);
b.addEventListener('click',function(){
  e.stopPropagation()
  alert('触发了b');
},true);
c.addEventListener('click',function(){
  alert('触发了c');
},true)

在b的事件捕获中增加一个拦截。e.stopPropagation()

则会执行到这里拦截整个事件流,后续的事件都不再执行

这里您大概就懂了。这里再继续拓展一个概念,可以在面试中加更多分:

DOM事件处理中分为4个级别:DOM0级事件处理,DOM1级事件处理,DOM2级事件处理和DOM3级事件处理。

其中DOM1级事件处理标准中并没有定义相关的内容,所以没有所谓的DOM1事件处理;DOM3级事件在DOM2级事件的基础上添加了更多的事件类型。

之前学习原生中,dom绑定onClick事件属于DOM0级。DOM0级只支持冒泡阶段。

进一步规范之后,有了DOM2级事件处理程序,其中定义了两个方法:

addEventListener() ---添加事件侦听器 removeEventListener() ---删除事件侦听器

addEventListener这个东西 比onClick可以绑定更多的事件。第三个参数boolean默认false表示使用冒泡机制,true表示捕获机制

到了这里,来个干货场景

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>

如果要给li注册一个点击事件,打印出它自身的innerHtml,你会怎么做?

有人就写了这样的一个代码

    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)
        })
    }

遍历li 然后注册咯。这样可以吗?可以!但是性能不好

正常情况我们给每一个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返回的是绑定事件的元素

因此我们总结一下事件委托的优点:

  1. 提高性能:每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少。
  2. 动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以一样具有和其他元素一样的事件。

结束~