👀 理一波 Javascript 事件机制【回顾 Plan】

1,014 阅读7分钟

QQ图片20210415165431.jpg

前言

说起Javascript事件,你想到的是什么呢? 我想答案里免不了有,事件捕获,冒泡,代理之类的词在里面。

但是是否真的理解到位了呢?

javascript本身作为一门事件驱动型的语言,可见事件的重要性。

如果用户在网页上单击一个按钮,你可能想通过显示一个信息框来响应这个动作 这个就是 事件,好了稍微了解一下事件的概念,就让我们开始接下来的文章吧。

放一下文章导图:

QQ图片20210415152437.png

还是一样习惯以问题开始:

  1. 能说说你知道的事件机制吗?
  2. 捕获冒泡,简单描述一下?
  3. 能说说事件流(事件传播)吗?
  4. 能说说事件代理吗?
  5. addEventListener(),三个参数分别是什么?第三个参数默认值,默认行为?
  6. 只需要一个指定的子组件捕获到事件,怎么做?如何阻止默认行为?
  7. JavaScript 怎么为事件绑定监听函数?
  8. 使用事件委托要注意什么问题?怎么解决?
  9. 不同子元素需要不同事件怎么处理?
  10. 使用removeEventListener()要注意什么呢?

希望你会,或者看完文章的你会😄

(因为下面的例子会涉及到一些监听事件已经事件对象相关的知识,所以先介绍一下)

一、事件监听

在浏览器中,事件模型就是通过监听函数对事件做出反应。事件发生后,浏览器监听到这个事件,就会执行对应的监听函数——事件驱动编程模式

在javascript中有三种方式,可以为事件绑定监听函数:

1.1 事件处理器属性

举个栗子:

栗子1window.onload = doSomething;//注意一下这里的doSomething
 
栗子2:div.onclick = function(event) {
    console.log('触发事件');
  };

这个onload, onclick 是被用在这个情景下的事件处理器的属性,它就像其他的属性一样,但是有一个特别的地方——当您将一些代码赋值给它的时候,只要事件触发代码就会运行(像栗子2)

1.2 行内事件处理器

举个栗子:

<button onclick="doSomething()">Press me</button> //注意这里的doSomething()

"doSomething()"属性值实际上是当事件发生时要运行的JavaScript代码。也可以直接在里面写入javascript代码(但混用 HTML 和 JavaScript,文档很难解析,不好哈)。

1.3 addEventListener() 和removeEventListener()

“DOM2级事件”定义了两个方法,用于处理指定混合删除事件处理程序的操作:addEventListener(),removeEventListener(),所有的节点中都包含这两个方法,并且都有三个参数:

  1. 要处理的事件名
  2. 作为事件处理程序的函数
  3. 一个布尔值(true:捕获阶段调用函数;false:冒泡阶段调用函数
  • 同一个监听器addEventListener注册多个处理器(handler,handler1) 当我们使用上面的事件处理器属性方法尝试去注册多个处理器:
 var btn = document.getElementById("myBtn");
  var handler = function() {
    console.log("我是handler()处理的");
  }
  var handler1 = function() {
    console.log("我是handler1()处理的");
  }
  btn.onclick = handler;
  btn.onclick = handler1;

image.png

只打印了一个结果,因为后面任何设置的属性都会尝试覆盖较早的属性,使用addEventListener解决这个问题:

 var btn = document.getElementById("myBtn");
  var handler = function() {
    console.log("我是handler()处理的");
  }
  var handler1 = function() {
    console.log("我是handler1()处理的");
  }
  btn.addEventListener("click",handler,false);
  btn.addEventListener("click",handler1,false);

image.png

  • removeEventListener()的使用 通过addEventListener(),而removeEventListener()则是用来移除,并且需要注意的是传入的参数与添加处理程序时使用的参数相同。
正确的:
btn.addEventListener("click",handler,false);
btn.removeEventListener("click",handler,false);

错误的:
btn.addEventListener("click",handler,false);
btn.removeEventListener("click",handler1,false);

并且匿名函数也是不行的

 btn.addEventListener("click",function() {
    console.log(1)
  },false);
 btn.removeEventListener("click",function() {
    console.log(2)
  },false);

二、事件对象

在触发DOM上的某个事件时,会产生一个事件对象Event,这个对象中包含着所有与事件有关的信息,包括导致事件的元素,事件的类型,以及其他与特定事件相关的信息。

并且Event对象本身就是一个构造函数,可以用来生成实例。

event = new Event(type, options);

那作为一个构造函数,就有它自己的一些属性和方法,下面围绕这个展开一下:(之后的例子会用到一些,所以这里可以先了解一下)

2.1 属性

  • bubbles 表明事件是否冒泡 (Event构造函数生成的事件,默认是不冒泡的)
  • target 可以返回事件的目标节点,target就可以表示为当前的事件操作的dom
  • eventPhase调用事件处理程序的阶段: 1——表示捕获阶段

2——表示“处于目标”

3——表示冒泡阶段

<body>
  <button id="myBtn">按钮</button>
</body>
<script>
  var btn = document.getElementById("myBtn");
  btn.onclick = function(event) {
    console.log(event.eventPhase,"btn.onclick");
  }
  document.body.addEventListener("click",function(event) {
    console.log(event.eventPhase,"document.body.addEventListener")
  },true)
  document.body.onclick = function(event) {
    console.log(event.eventPhase,"document.body.onclick");
  }

image.png

  • cancelable 是否取消事件的默认行为
  • defaultPrevented 为true表示已经调用了preventDefault()
  • currentTarget 事件处理程序正在处理时间的那个元素
  • target 事件的目标

有关一个this,currentTarget,target的指向问题:

在事件处理程序内部,对象this始终等于currentTarget,而target则只包含事件的实例目标,当直接指定了目标元素 -- 下面例子的btn.onclick,则this,currentTarget,target包含相同值。

 var btn = document.getElementById("myBtn");
  btn.onclick = function(event) {
    console.log(event.currentTarget == this); //true
    console.log(event.target == this); //true
  }

如果不直接指定目标元素:

event.currentTarget,this都指向了document.body,而event.target却指向了document.getElementById("myBtn"),因为它是click事件的目标。

因为按钮上没有注册事件处理程序,结果click事件冒泡到了document.body,在那里事件得到了处理。

 var btn = document.getElementById("myBtn");
  document.body.onclick = function(event) {
    console.log(event.currentTarget === document.body);
    console.log(this === document.body);
    console.log(event.target === document.getElementById("myBtn"));

  }

image.png

2.2 方法

  • Event.preventDefault() 取消事件的默认行为,配合cancelable为true使用
  • Event.stopPropagation() 取消事件的进一步捕获或冒泡,配合bubbles为true使用
  • Event.composedPath() 返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。
  • Event.stopImmediatePropagation() 取消事件的进一步捕获或冒泡,同时阻止任何事件处理函数被调用。(有点像stopPropagation()的升级版)

三、事件机制

3.1 事件冒泡

事件开始时,由具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点。

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Document</title>
</head>
<body>
  <div id="myDiv">按钮</div>
</body>
</html>

如果单击了页面中的

元素,那么这个click事件会按照如下顺序传播:

graph TD
Element-div --> Element-body --> Element-html --> Document

click事件首先在div元素上发送,而这个元素就是我们单击的位置,然后click事件沿着DOM树向上(这里我图画的不是很好,本来应该向上的)传播,在每一级节点是哪个都会发生,一直到 document对象。

3.2 事件捕获

事件捕获是不太具体的节点更早接收到事件,而具体的节点应该最后接收到事件,在花时间到达预定目标之前捕获它。

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Document</title>
</head>
<body>
  <div id="myDiv">按钮</div>
</body>
</html>

如果单击了页面中的

元素,那么这个click事件会按照如下顺序传播:

graph TD
Document --> Element-html --> Element-body --> Element-div

在事件捕获中,document对象先接收到click事件,然后沿着DOM树依次向下,一直到事件的实际目标。

四、事件流(事件传播)

一个事件发生后,会在父元素和子元素中进行传播,这种传播也叫事件流,分成三个阶段:

  1. 从window对象传导到目标节点,称为捕获阶段
  2. 在目标节点上触发,称为“目标阶段”
  3. 从目标节点传回window对象,称为“冒泡阶段”

(下图的箭头,先向下捕获再往上冒泡)

graph TD
Document --> Element-html --> Element-body --> Element-div --> Element-body --> Element-html --> Document

结合上面的属性方法和已知的冒泡捕获的概念理解,来看看下面这个例子吧:

<body>
  <div>
    <p>点击</p>
  </div>
</body>

<script>
  var phases = {
  1: '捕获',
  2: '目标',
  3: '冒泡'
};

var div = document.querySelector('div');
var p = document.querySelector('p');

div.addEventListener('click', callback, true); // 捕获时调用callback
p.addEventListener('click', callback, true); // 捕获时调用 callback
div.addEventListener('click', callback, false); // 冒泡时调用 callback
p.addEventListener('click', callback, false); // 冒泡时调用 callback

function callback(event) {
  var tag = event.currentTarget.tagName; // 指定目标的标签名 div p
  var phase = phases[event.eventPhase]; //调用事件处理程序的阶段
  console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
</script>

image.png

分析一下:

  1. 由上图在控制台打印的结果,可以看出click事件被触发了四次:
    节点的捕获阶段和冒泡阶段各1次,

    节点的目标阶段触发了2次。

  2. 三个阶段 注意

在捕获阶段依次为window、document、html、body、div、p;

在冒泡阶段依次为p、div、body、html、document、window;

(下面我只把div和p画了图哦)

  • 捕获阶段:触发了div的click事件
graph TD
Element-div --> Element-p 
  • 目标阶段: 事件到达p,触发了p的click事件

  • 冒泡阶段:再次触发div的click事件

graph TD
Element-p --> Element-div
  1. 监听函数
p.addEventListener('click', callback, true);
p.addEventListener('click', callback, false);

p节点上有个监听函数,他们都会因为click事件触发,因此在目标阶段有两次输出

五、事件委托(事件代理)

在javascript中,添加到页面的时间处理程序将直接关系到页面的整体运行性能,而对于“事件处理程序过多”问题的解决方案就是——事件委托,也是我们说的事件代理。

(希望你已经看完了,或者说已经理解文章前面的知识点,这对理解事件委托机制有很大帮助)

5.1 初探

好吧,先来个例子,把不用事件委托和用事件委托来做个比较:

<ul id="link">
    <li>1</li>
</ul>

实现点击li,触发事件处理程序

不用事件委托的情况:

可以直接在li标签上定义事件,但是这个操作就需要先找到ul标签,再遍历里标签(ul1.getElementsByTagName('li')得到的是数组),点击li的时候,找到目标的li的位置才可执行操作。而且每次都要重复找li标签。

 var ul1 = document.getElementById('link');
    var li1 = ul1.getElementsByTagName('li');

    for(var i=0;i<li1.length;i++){
      li1[i].addEventListener("click",function() {
        console.log("在li元素上加事件处理程序")
    })
    }

使用事件委托:

    var ul1 = document.getElementById('link');
    var li1 = ul1.getElementsByTagName('li');

    ul1.addEventListener("click",function() {
        console.log("在ul元素上加事件处理程序")
    })

这里用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发

但是这里有个小问题哈,就是点击ul也是有触发事件的,我怎么才能只点击li的时候触发,在点击ul的时候不触发呢?

5.2 完善

这个问题就要结合我们之前说的target,在下面这段代码中,我们使用了事件委托只为ul元素添加了监听事件,由于li是这个元素的子节点,而且它的事件会冒泡,所以单击事件最终会被这个函数处理。

我们知道,事件目标是被单击的列表项,event.target指向的就是点击的列表项。

 var ul1 = document.getElementById('link');
    // var li1 = ul1.getElementsByTagName('li');
      ul1.addEventListener("click",function(event){
    var event = event || window.event;
    var target = event.target;
    if(target.nodeName.toLowerCase() == 'li'){
         console.log(123);
         console.log(target.innerHTML);
    }
  }) 

5.3 升级

这个是一个列表项,如果我需要为三个li添加三个不同的事件呢?:

 <ul id="myLinks">
    <li id="jingda1">jingda1</li>
    <li id="jingda2">jingda2</li>
    <li id="jingda3">jingda3</li>
  </ul>
  • 不用事件委托:
<script>

  var item1 = document.getElementById('jingda1')
  var item2 = document.getElementById('jingda2')
  var item3 = document.getElementById('jingda3')
  
  item1.addEventListener("click",function() {
    console.log(1)
  })
  item2.addEventListener("click",function() {
    console.log(2)
  })
  item3.addEventListener("click",function() {
    console.log(3)
  })
</script>
  • 使用事件委托
var ul1 = document.getElementById('myLinks');
      ul1.addEventListener("click",function(event){
    var event = event || window.event;
    var target = event.target;
        // console.log(target.id)

      switch(target.id) {
        case 'jingda1':
          console.log("jingda1");
          break;
        case 'jingda2':
          console.log("jingda2");
          break;
        case 'jingda3':
          console.log("jingda3");
          break;
      }

事件目标是被单击的列表项,event.target指向的就是点击的列表项,通过检测target.id属性来决定采用适当的操作。

总结

有关javascript事件相关的知识就先说到这里了,有很多东西可能并没有总结到。或者说是有错的地方,希望多多建议

(还有一个大类我没有总结-javascript的事件类型,希望各位有时间也可以理一下,这篇文章我还是重点想把事件机制拉出来讲。)

关于javascript事件这个话题,可能平时用的并不是很多,但是我们必须要了解。比如在学有关react知识的时候,有关setSate是异步还是同步这个问题,中间会涉及到合成事件。而要深入理解这个知识点就必须先要理解javascript的事件机制。【这似乎又是一个小盲区,头秃】

image.png

好啦,希望看完文章的你或多或少对javascript事件又多了一些了解。可能还有很多疑问的地方,最后推荐几个我在学习和参考的相关知识点的书籍和文章。

我是婧大,一名大三学崽,希望和你一起学习一起进步。🙆🙆🙆

加油!

文章肯定有写的不好的地方,欢迎评论区指正。

写的不全的地方,也建议大家阅读一下后面的参考文献呀👇👇👇

image.png

【参考文章💬】

Javascript 高级教程

Web API 接口参考-Event

javascript教程- 事件

js中的事件委托或是事件代理详解