JS基础 - 事件

399 阅读12分钟

Web页面需要经常和用户之间进行交互,而交互的过程中我们可能想要捕捉这个交互的过程,并进行相应的处理

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

浏览器会监听某个事件的发生,当某个事件发生时,让JavaScript可以响应(执行某个函数,回调某个函数)

所以我们只需要针对事件编写处理程序(handler)

事件绑定方式

存在如下html结果

<button id="btn">ckick me</button>

方式一: 在元素中直接监听(很少使用)

<!-- 浏览器原生的事件名都是小写的 -->
<button onclick="console.log('button was clicked')">ckick me</button>

方式二:DOM属性,通过元素的on来监听事件

const btnEl = document.getElementById('btn')

// onclick 是 dom的一个属性 对应的值是事件响应函数
// 这也就意味着如果赋值了多个事件的话,最新的事件会把之前的事件函数给覆盖
// 也就是使用这种方式无法绑定多个同类型事件
btnEl.onclick = () => console.log('button was clicked')

方式三:通过EventTarget中的addEventListener来监听

const btnEl = document.getElementById('btn')

// addEventListener是从EventTarget继承过来的属性,所以每一个node都可以使用该方法来绑定事件
// 1. 该事件第一个参数为标准事件名(没有on前缀) 第二个参数为事件响应函数
// 2. addEventListener可以给某一个元素 绑定多个同类型事件
// addEventListener会将多个事件处理对象放置到一个数组中,在执行的时候,遍历取出数组中的函数并执行
btnEl.addEventListener('click', () => console.log('按钮第一次被点击'))
btnEl.addEventListener('click', () => console.log('按钮第二次被点击'))

冒泡和捕获

当我们在浏览器上对着一个元素点击时,我们点击的往往不仅仅是这个元素本身

因为html元素之前是存在嵌套层级的,所以当我们触发了子元素的某个事件时候,其实其所有的父级元素所对应的事件都会被触发

比如一个span元素是放在div元素上的,div元素是放在body元素上的,body元素是放在html元素上的

所以当我们点击了span元素的时候,div元素和body元素,html元素所对应的点击事件都会以此被触发

而事件的传递顺序,或者说是事件的触发顺序,就被称之为事件流

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

事实上,还有另外一种监听事件流的方式就是从外层到内层(body -> span),这种称之为事件捕获(Event Capture)

所以本质上事件捕获和事件冒泡本质就是事件流的两种不同的传播方式

image.png

事件阶段说明
捕获阶段(Capturing phase)事件(从 Window)向下进行传递
早期NetSpace采用的事件流形式
目前虽然依旧存在,但是很少使用
目标阶段(Target phase)事件到达目标元素
冒泡阶段(Bubbling phase)事件从元素上开始冒泡
早期IE采用的事件流模式
也是目前大多数浏览器使用的事件流模式
const btnEl = document.getElementById('btn')

// addEventListener方法第三个参数默认值为false
// 当其值为false的时候,表示该事件响应函数在事件的冒泡阶段触发
// 当其值为true的时候,表示该事件响应函数在事件的捕获阶段触发
btnEl.addEventListener('click', e => {
  // eventPhase是事件处理对象中的一个属性,其值是Number类型值
  // 0 - 事件为被处理 1 - 事件处于捕获中 2 - 事件被触发 3 - 事件处于冒泡阶段
  console.log(e.eventPhase) // => 2
  console.log('button被点击了')
}, true)

事件对象

当一个事件发生时,就会有和这个事件相关的很多信息,比如事件的类型是什么,你点击的是哪一个元素,点击的位置是哪里等等相关的信息,我们可能会需要去获取这些信息

于是浏览器将这些信息会被封装到一个Event构造函数中,并创建对应的对象以供使用,也就是event对象

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

event常见属性和方法

属性说明
type事件类型
target触发事件的元素,即事件最初发生的元素
currentTarget表示当前正在处理事件的元素
事件存在冒泡和捕获,所以只有事件被触发时,currentTarget才会等价于target
offsetX、offsetY触控点相对于currentTarget左上角的坐标
clientX、clientY触控点对于视口左上角的坐标
pageX、pageY触控点相对于页面左上角的坐标
页面可能被卷曲出去 所以值可能为负数
screenX、screenY触控点相对于 显示屏左上角的坐标
方法说明
preventDefault1. 取消事件的默认行为
stopPropagation1. 阻止事件的进一步传递
2. 冒泡或者捕获都可以阻止

阻止元素默认行为

const aEl = document.getElementById('a')

// 如果元素同时具有自定义行为,也有默认行为
// 自定义行为会先执行,再执行元素的默认行为
aEl.addEventListener('click', e => {
  console.log('超链接被点击了')
  // 阻止默认行为
  e.preventDefault()
})

事件中的this

在函数中,我们也可以通过this来获取当前的发生元素

const outerEl = document.getElementById('outer')
const innerEl = document.getElementById('inner')

innerEl.addEventListener('click', function (e) {
  // 监听函数内部是没有this的,所以如果要在事件回调中使用this
  // 对应的事件处理函数一定要普通函数

  // 如果事件回调中可以使用this,那么this就是当前处理该事件回调的对象
  // 也就是说就是 e.currentTarget
  console.log(this === e.currentTarget) // => true
})

EventTarget

EventTarget是一个DOM接口,主要用于添加、删除、派发Event事件

所有的节点、元素都继承自EventTarget, 包括window对象

方法说明
addEventListener注册某个事件类型以及事件处理函数
removeEventListener移除某个事件类型以及事件处理函数
dispatchEvent使用JavaScript来主动触发某个事件
const outerEl = document.getElementById('outer')
const innerEl = document.getElementById('inner')

const handleClick = () => {
  console.log('handleClick')
}

// 添加事件
innerEl.addEventListener('click', handleClick)

// 移除事件
// 注意: 移除事件所传入的那个事件函数(也就是第二个参数)
// 必须和添加事件中所传入的那个事件函数是同一个对象
// 因为其内部使用的是类似于find或findIndex的机制
// 来在事件队列中查找所需要移除的那个事件处理函数
// 如果绑定的事件处理函数和移除的事件处理函数不是同一个函数
// 那么将无法正常移除对应的事件处理函数
innerEl.removeEventListener('click', handleClick)
const outerEl = document.getElementById('outer')
const innerEl = document.getElementById('inner')

// 监听原生事件
window.addEventListener('click', () =>  console.log('click'))

// 监听自定义事件
innerEl.addEventListener('customEvent', e => console.log('customEvent', e))

// 我们可以使用dispatchEvent方法来手动触发事件 
// dispatchEvent也存在于EventTarget上,所以所有node节点和window上都可以调用该方法
// dispatchEvent的参数为需要传递给对应事件处理函数的event对象,所以参数类型是一个event实例
// 我们可以使用new Event(type) 来创建一个事件对象,参数为对应的事件类型

// 什么元素监听的时候,对应派发事件的时候也需要使用什么元素去派发
// 如果派发某个元素的事件的时候,对应的元素并没有监听对应的事件,也就是没有对应的事件处理函数
// 那么就会静默失效
window.dispatchEvent(new Event('click'))
innerEl.dispatchEvent(new Event('customEvent'))

事件委托 (event delegation)

在绝大情况下,我们可以借助事件冒泡来简化我们对应的事件处理

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

这就是事件委托,其实事件委托本质也是一种设计模式(编程思想),那就是代理模式(委托模式)

使用事件委托,我们可以将子元素中对应的事件处理,统一放到父元素的事件处理函数中进行处理

这样可以减少对应的事件绑定,也就是说不是使用事件委托,依旧可以完成对应的功能

但是使用事件委托后,可以简化事件处理流程,提高对应的性能

案例一 --- 点击li,让被点击的li字体变红,其余li字体保持黑色

实现方式一 - 使用循环来进行排他

const ulEl = document.getElementById('ul')

// 将li对应的事件处理统一放置到ul中进行处理
// 我们就只需要绑定一次对应的事件处理函数
// 而不用为每一个li都去绑定对应的事件处理函数
// 这就是事件委托,事件委托可以简化事件处理流程,提高性能
ulEl.addEventListener('click', e => {
  // 避免在ul上添加active类,导致所有的li的字体全部变成红色
  // 也可以使用 e.target.tagName !== 'UL' 来移除ul元素
  if (e.target !== ulEl) {
    for (const liEl of ulEl.children) {
      liEl.classList.remove('active')
    }

    // 通过e.target获取到实际触发事件的那个元素
    e.target.classList.add('active')
  }
})

实现方式二 - 使用querySelector方法简化遍历流程

const ulEl = document.getElementById('ul')

ulEl.addEventListener('click', e => {
  if (e.target !== ulEl) {
    // 虽然使用document.querySelector的本质依旧是在进行元素的遍历
    // 但是该遍历是浏览器来自主完成,可以简化对应的代码
    const activeEl = document.querySelector('.active')

    // 这里需要使用可选链,因为默认情况下,初始执行的时候
    // 所有的li元素都是没有active类的,也就是说activeEl的值是null
    activeEl?.classList.remove('active')

    e.target.classList.add('active')
  }
})

实现方式三 - 使用临时变量来避免循环

const ulEl = document.getElementById('ul')

let activeEl = null

ulEl.addEventListener('click', e => {
  if (e.target !== ulEl) {
    // 使用临时变量来记录有active类的li元素
    // 从而避免循环查找,从而提高性能
    if (!activeEl) {
      activeEl = e.target
    } else {
      activeEl.classList.remove('active')
    }

    e.target.classList.add('active')

    activeEl = e.target
  }
})

事件委托的标记

某些事件委托可能需要对具体的子组件进行区分,这个时候我们可以使用data-*对其进行标记,这个标记就被称之为事件委托的标记

<div id="outer" class="outer">
  <button data-action="add">添加</button>
  <button data-action="change">修改</button>
  <button data-action="remove">移除</button>
</div>
const outerEl = document.getElementById('outer')

outerEl.addEventListener('click', e => {
  // dateset是html元素所对应的dom对象的属性
  // 这里的action就是事件委托标记
  const action = e.target.dataset.action

  switch (action) {
    case 'add':
      console.log('add clicked')
      break
    case 'change':
      console.log('change clicked')
      break
    case 'remove':
      console.log('remove clicked')
  }
})

常见事件

鼠标事件

image.png

mouseenter和mouseleave

mouseenter和mouseleave是一对 对应的函数

  • 不支持冒泡
  • 子元素是父元素的一部分 是一个整体 , 所以进入子元素依然属于在该元素内,没有任何反应

image.png

image.png

mouseover和mouseout

mouseover和mouseout是一对 对应的函数

  • 支持冒泡 --- 所以支持事件委托
  • 进入元素的子元素时
    • 先调用父元素的mouseout
    • 再调用子元素的mouseover
    • 因为支持冒泡,所以会将mouseover传递到父元素中,于是父元素的mouseover事件会被再次触发

image.png

image.png

补充

<!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>
    .container {
      width: 300px;
      height: 300px;
      background-color: skyblue;
      position: relative;
      margin: 100px auto;
    }

    .box {
      width: 100px;
      height: 100px;
      background-color: red;
      position: absolute;
      left: 0;
      top: 0;
      transform: translateX(-100%);
    }
  </style>
</head>
<body>
  <div class="container">
    lorem
    <div class="box">demo</div>
  </div>

  <script>
    const containerEl = document.querySelector('.container')
    const boxEl = document.querySelector('.box')

    /*
      事件流的流向 是由dom结构决定的,与cssom无关

      所以即使通过样式将div.box 移到了 div.container外,
      div.box的mouseover和mouseout事件依旧会被触发
    */
    boxEl.addEventListener('mouseover', () => console.log('inner over'))
    boxEl.addEventListener('mouseout', () => console.log('inner out'))

    containerEl.addEventListener('mouseover', () => console.log('outer over'))
    containerEl.addEventListener('mouseout', () => console.log('outer out'))
  </script>
</body>
</html>

键盘事件

image.png

事件的执行顺序是 onkeydown、onkeypress、onkeyup

keydown事件是在键盘诶按下的时候被触发

keypress事件是在对应的内容准备输入到浏览器中的时候被触发 「 已被废弃 」

所以keydown事件一般先于keyup事件被触发

const inputEl = document.getElementById('input')

inputEl.addEventListener('keyup', e => {
  // key -> 字符("A","a" 等),对于非字符(non-character)的按键,绝大多数通常具有与 code 相同的值,但并不是全部
  // code -> 键位 "按键代码”("KeyA","ArrowLeft" 等),特定于键盘上按键的物理位置
  console.log(e.key, e.code)
  /*
    => 输出结果示例:
     key     code
     a       KeyA
     A       KeyA
     Enter   Enter
     Control ControlLeft
  */
})

表单事件

image.png

const inputEl = document.getElementById('input')

// 表单输入元素的input事件和change事件
// 只要有输入内容就会触发input事件,也就是每输入一个字符,就会触发一次input事件
// 只要内容发生了改变就会触发change事件,也就是只有内容完全输入完毕,例如输入框失去焦点的时候,才会触发change事件
document.body.addEventListener('input', () => console.log(inputEl.value, 'input'))
document.body.addEventListener('change', () => console.log(inputEl.value, 'change'))
const formEl = document.getElementById('form')

// 对于表单提交和重置事件,虽然是按钮触发的对应事件,但是实际执行这些事件的是form元素
// 所以对于submit事件和reset事件,是在form元素上进行监听的
// 普通元素(包括除form元素外的所有表单元素)是没有submit事件和reset事件的
formEl.addEventListener('submit', () => console.log('submit'))
formEl.addEventListener('reset', () => console.log('reset'))

文档加载事件

事件名说明
DOMContentLoaded浏览器已完全加载 HTML,并构建了 DOM 树
但像 <img> 和样式表之类的外部资源可能尚未加载完成
load浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等

视图事件

事件名说明
resize视口或元素尺寸发生了改变的时候,会触发对应的事件
scroll视口或元素存在滚动条且存在滚动行为时,会触发对应的事件

网络事件

事件名说明
online浏览器已获得网络访问
offline浏览器已失去网络访问

焦点事件

事件名说明
focus元素获得焦点(不会冒泡)
blur元素失去焦点(不会冒泡)

transition事件 (过渡事件)

事件名说明
transitionstart开始进行CSS过渡
transitionendCSS过渡已经完成
transitioncancelCSS过渡被取消

animation事件 (动画事件)

事件名说明
animationstart某个 CSS 动画开始时触发
animationend某个 CSS 动画完成时触发
animationiteration循环播放CSS动画时,重新开始一个新的CSS动画时会被触发