JavaScript事件处理

98 阅读7分钟

认识事件处理

Web页面需要经常和用户之间进行交互,而交互的过程中我们可能想要捕捉这个交互的过程。比如用户点击了某个按钮、用户在输入框里面输入了某个文本、用户鼠标经过了某个位置。

浏览器需要搭建一条JavaScript代码和事件之间的桥梁。

当某个事件发生时,让JavaScript可以相应(执行某个函数),所以我们需要针对事件编写处理程序(handler)。

如何进行事件监听呢?

  • 在script中直接监听 (很少使用)
  • DOM属性,通过元素的on来监听事件
  • 通过EventTarget中的addEventListener来监听
<body>
  // 直接在html中编写JavaScript代码(了解)
  // 这种方式很不常用,因为代码复杂之后阅读性不好
  <button onclick="console.log('按钮1发生了点击~');">按钮1</button>

  <button class="btn2">按钮2</button>
  <button class="btn3">按钮3</button>

  <script>

    // 1.获取元素对象
    var btn2El = document.querySelector(".btn2")
    var btn3El = document.querySelector(".btn3")

    // 2.onclick属性,这种方法的缺点在于无法执行多个函数,如果你硬要写两个函数,后面的会将前面的覆盖
    function handleClick01() {
      console.log("按钮2发生了点击~")
    }
    btn2El.onclick = handleClick01

    // 3.addEventListener(推荐),下面三个函数均会执行,因此这种方式更灵活
    btn3El.addEventListener("click", function() {
      console.log("第一个btn3的事件监听~")
    })
    btn3El.addEventListener("click", function() {
      console.log("第二个btn3的事件监听~")
    })
    btn3El.addEventListener("click", function() {
      console.log("第三个btn3的事件监听~")
    })

  </script>

事件的冒泡和捕获

对于事件件有一个概念叫做事件流,为什么会产生事件流呢?当我们在浏览器上对着一个元素点击时,你点击的不仅仅是这个元素本身,我们的HTML元素是存在父子元素叠加层级的,假如一个span元素是放在div元素上的, div元素是放在body元素上的, body元素是放在html元素上的,那么当你点击span元素的时候,上层所有的父元素都发生了点击。下面是一个小例子

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 200px;
      height: 200px;
      background-color: orange;
    }

    .box span {
      width: 100px;
      height: 100px;
      background-color: red;
    }
  </style>
</head>
<body>
  
  <div class="box">
    <span></span>
  </div>

  <script>
    // 1.获取元素
    var spanEl = document.querySelector("span")
    var divEl = document.querySelector("div")
    var bodyEl = document.body

    // 默认情况下是事件冒泡
    spanEl.addEventListener("click", function() {
      console.log("span元素发生了点击~冒泡")
    })
    divEl.addEventListener("click", function() {
      console.log("div元素发生了点击~冒泡")
    })
    bodyEl.addEventListener("click", function() {
      console.log("body元素发生了点击~冒泡")
    })

    // 设置希望监听事件捕获的过程(第三个参数加上true)
    spanEl.addEventListener("click", function() {
      console.log("span元素发生了点击~捕获")
    }, true)
    divEl.addEventListener("click", function() {
      console.log("div元素发生了点击~捕获")
    }, true)
    bodyEl.addEventListener("click", function() {
      console.log("body元素发生了点击~捕获")
    }, true)

  </script>
</body>
</html>

我们会发现默认情况下事件是从由内向外依次传递的顺序,这个顺序我们称之为事件冒泡(Event Bubble),

事实上,还有另外一种监听事件流的方式就是由外向内, 这种称之为事件捕获(Event Capture) ;

为什么会产生两种不同的处理流呢?这是因为早期浏览器开发时,不管是IE还是Netscape公司都发现了这个问题;

但是他们采用了完全相反的事件流来对事件进行了传递;IE采用了事件冒泡的方式, Netscape采用了事件捕获的方式。

注意:事件的捕获和监听浏览器都会执行(按照捕获-->监听的顺序),只不过看你监听的是哪个(上面的代码就是同时监听两个过程)。

开发中通常会使用事件冒泡,所以事件捕获了解即可。

事件对象Event

一个事件发生时,就会有和这个事件相关的很多信息:比如事件的类型是什么,你点击的是哪一个元素, 点击的位置是哪里等等相关的信息;那么这些信息会被封装到一个Event对象中,这个对象由浏览器创建,称之为event对象;该对象给我们提供了想要的一些属性,以及可以通过该对象进行某些操作。

如何获取这个event对象呢?

event对象会在传入的事件处理函数回调时,被系统传入;我们可以在回调函数中拿到这个event对象;

event对象的属性有type:事件的类型;target:当前事件发生的元素;currentTarget:当前处理事件的元素;eventPhase:事件所处的阶段;

还有offsetX、 offsetY、clientX、 clientY、pageX、 pageY、screenX、 screenY,这些就基本上用不到了。

但是其中target和currentTarget两个方法一定要区分,先看下面一段代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      display: flex;
      width: 200px;
      height: 200px;
      background-color: orange;
    }

    span {
      width: 100px;
      height: 100px;
      background-color: #f00;
    }
  </style>
</head>
<body>

  <div class="box">
    <span class="btn">
      <button>按钮</button>
    </span>
  </div>
    
  <script>
    var divEl = document.querySelector("div")

    divEl.onclick = function(event) {
      console.log(event.target)
      console.log(event.currentTarget)
      console.log(event.currentTarget === event.target)
    }

  </script>
</body>
</html>

可以看到上面的代码我们监听的就是div元素,这时候当我们点击div,第三个输出会是true,意味着这时候target和currentTarget是相等的,但是当我们点击的是span,由于存在事件冒泡,div元素同样能监听到点击,但是事件发生的地方已经不是div了而是span,这个时候二者不相等了,target属性是获取实际发生事件的元素(被点击的那个元素span),而currentTarget属性获取的是处理事件的元素(我们监听的元素div)。

event对象常见的方法:preventDefault: 取消事件的默认行为;stopPropagation:阻止事件的进一步传递(冒泡或者捕获都可以阻止)。

一般情况下也是没有必要用到这两个方法,这里具体的用法就不解释了。

补充:回调函数

什么是回调函数?

作为参数被传递给另一个函数的函数叫作回调函数。

为什么需要回调函数?

JavaScript 按从上到下的顺序运行代码。但是,在有些情况下,必须在某些情况发生之后,代码才能运行(或者说必须运行),这就不是按顺序运行了。这是异步编程。

回调函数确保:函数在某个任务完成之前不运行,在任务完成之后立即运行。它帮助我们编写异步 JavaScript 代码,避免问题和错误。

在 JavaScript 里创建回调函数的方法是将它作为参数传递给另一个函数,然后当某个任务完成之后,立即调用它。

如何创建一个回调函数?

const message = function() {  
    console.log("This message is shown after 3 seconds");
}
 
setTimeout(message, 3000);

上面函数的作用是在控制台打印一条消息(message),它在 3 秒之后显示。

message 函数是在发生某事之后(在本示例中为 3 秒之后),而不是在此之前被调用。因此,message 函数就是一个回调函数

匿名函数也可以作为回调函数

setTimeout(function() {  
    console.log("This message is shown after 3 seconds");
}, 3000);
// 这里的回调函数没有名称

也可以用箭头函数写回调函数

setTimeout(() => { 
    console.log("This message is shown after 3 seconds");
}, 3000);

事件委托

当子元素被点击时,父元素可以通过冒泡可以监听到子元素的点击;并且可以通过event.target获取到当前监听的元素;

案例:一个ul中存放多个li,点击某一个li会变成红色

方案一:监听每一个li的点击,并且做出相应

方案二: 在ul中监听点击,并且通过event.target拿到对应的li进行处理(因为这种方案并不需要遍历后给每一个li上添加事件监听,所以它更加高效)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .active {
      color: red;
      font-size: 20px;
      background-color: orange;
    }
  </style>
</head>
<body>
  
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ul>

  <script>
    // 每一个li都监听自己的点击, 并且有自己的处理函数
    var liEls = document.querySelectorAll("li")
    for (var liEl of liEls) {
      // 监听点击
      liEl.onclick = function(event) {
        event.currentTarget.classList.add("active")
      }
    }
  </script>
</body>
</html>

上面的所示的代码是第一种解决方案,但是效率低下,没有必要为每一个li创建一个处理函数,我们可以通过事件的冒泡统一在ul中监听,以下是改进的代码(同时增加了新功能):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .active {
      color: red;
      font-size: 20px;
      background-color: orange;
    }
  </style>
</head>
<body>
  
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ul>

  <script>
    // 统一在ul中监听
    // 新需求: 点击的li变成active, 其他的取消active
    var ulEl = document.querySelector("ul")
    var activeLiEl = null
    ulEl.onclick = function(event) {
      // 1.变量记录的方式
      if (activeLiEl) {
        activeLiEl.classList.remove("active")
      }
      // 给点击的元素添加active
      event.target.classList.add("active")

      // 记录最新的active对应的li
      activeLiEl = event.target
    }
  </script>
</body>
</html>

补充:classList 属性返回元素的类名,作为 DOMTokenList 对象。该属性用于在元素中添加,移除及切换 CSS 类。classList 属性是只读的,但你可以使用 add() 和 remove() 方法修改它。