Web APIs 学习第四天:DOM事件进阶

Web APIs 学习Day4

进一步学习事件进阶,实现更多交互的网页特效,结合事件流的特征优化事件执行的效率

  • 掌握阻止事件冒泡的方法
  • 理解事件委托的实现原理

文章目录

事件流

事件流是对事件执行过程的描述,是事件完整执行 过程中的流动路径。事件流是事件在执行时的底层机制,主要体现在父子盒子之间事件的执行上。

了解事件的执行过程有助于加深对事件的理解,提升开发实践中对事件运用的灵活度。

如上图所示,任意事件被触发时总会经历两个阶段:捕获阶段冒泡阶段

简言之,捕获阶段是【从父到子】的传导过程,冒泡阶段是【从子向父】的传导过程。

事件捕获

概念:从 DOM 的根元素开始去执行对应的事件(从外到里)

事件捕获需要写对应的代码才能看到效果,代码:

DOM.addEventListener(事件类型, 事件处理函数, 是否使用捕获机制)

说明:

  • addEventListener 第三个参数传入 true 代表是捕获阶段触发(当然在开发中我们很少使用这个参数)
  • 若传入 false 代表冒泡阶段,默认就是 false
  • 若是用 L0 事件监听(事件源.on事件类型 = function(){}),则只有冒泡阶段,没有捕获。早期 IE 也不支持捕获

了解了什么是事件捕获之后,我们来看事件捕获是如何执行的:

html 复制代码
<body>
  <div class="outer">
    <div class="inner">
      <div class="child"></div>
    </div>
  </div>
  <script>
    // 获取嵌套的3个节点
    const outer = document.querySelector('.outer');
    const inner = document.querySelector('.inner');
    const child = document.querySelector('.child');

    // 外层的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是爷爷')
    }, true)
    
    // 中间的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是爸爸')
    }, true)
    
    // 内层的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是儿子')
    }, true)
  </script>
</body>

执行上述代码后发现,当单击内层(儿子)盒子的事件触发时,其祖先元素的单击事件也相继触发,这是为什么呢?

结合事件流的特征,我们知道当某个元素的事件被触发时,事件总是会经过其祖先才能到达当前元素,然后再由当前元素向祖先传递,事件在流动的过程中遇到相同的事件便会被触发。

再来关注一个细节就是事件相继触发的【执行顺序】,事件的执行顺序是可控制的,即可以在捕获阶段被执行,也可以在冒泡阶段被执行。

如果事件是在冒泡阶段 执行的,我们称为冒泡模式 ,它会先执行子盒子事件再去执行父盒子事件,默认是冒泡模式。如果事件是在捕获阶段 执行的,我们称为捕获模式,它会先执行父盒子事件再去执行子盒子事件。

事件冒泡

概念:当一个元素的事件被触发时,同样的事件将会在该元素的所有组祖先元素中依次被触发,这个过程被称为事件冒泡。

事件冒泡其实是默认的。只不过在执行事件流时,祖先元素一般情况下都没有事件,因此看上去就像只发生了当前事件一样。

L2 事件监听第三个参数是 false,默认都是冒泡。L0 也是。

我们还是通过代码来了解一下:

html 复制代码
<body>
  <div class="outer">
    <div class="inner">
      <div class="child"></div>
    </div>
  </div>
  <script>
    // 获取嵌套的3个节点
    const outer = document.querySelector('.outer');
    const inner = document.querySelector('.inner');
    const child = document.querySelector('.child');

    // 外层的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是爷爷')
    })
    
    // 中间的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是爸爸')
    })
    
    // 内层的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是儿子')
    })
  </script>
</body>

你会发现控制台先打印'我是儿子',接着才是'爸爸'和'爷爷'。

阻止冒泡

其实很多时候我们都是不希望出现事件冒泡的,因此我们会主动去阻止冒泡的发生。

问题:因为默认就有冒泡模式的存在,所有容易导致影响到父级元素

若想把事件就限制在当前元素内,就需要阻止事件冒泡,而阻止事件冒泡需要拿到事件对象。

语法:事件对象.stopPropagation()

注意:此方法可以阻断事件流的传播,不光是在冒泡阶段有效,捕获阶段也有效。

因此,事实上不是阻止冒泡,而是直接阻止事件流!

html 复制代码
<body>
	 <div class="outer">
    <div class="inner">
      <div class="child"></div>
    </div>
  </div>
  <script>
	   // 获取嵌套的3个节点
    const outer = document.querySelector('.outer');
    const inner = document.querySelector('.inner');
    const child = document.querySelector('.child');

    // 外层的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是爷爷')
    })
    
    // 中间的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是爸爸')
    })
    
    // 内层的盒子添加事件
    outer.addEventListener('click', function () {
      console.log('我是儿子')
    })
    
    // 阻止事件冒泡
    e.stopPropagation()
  </script>
</body>

阻止默认行为

我们在某些情况下需要阻止默认行为的发生,比如,阻止链接的跳转、表单域跳转

语法:e.preventDefault()

举个例子,我们创建一个表单,表单里有一个提交表单的按钮,通常情况下,我们只要一点击就会提交表单,这就是表单域的默认行为。

html 复制代码
<form action="http://www.itcast.cn">
  <input type="submit" value="免费注册">
</form>

运行后,我们直接点击按钮即可跳转至网页。但是在实际开发中,如果用户没有填写表单就点击提交按钮,通常页面会跳出提示并阻止表单域跳转。这是怎么做到的呢?

其实这就是阻止表单的默认行为。我们可以:

html 复制代码
<form action="http://www.itcast.cn">
  <input type="submit" value="免费注册">
</form>
<script>
  const form = document.querySelector('form')
  form.addEventListener('submit', function(e){
    // 阻止表单默认行为 提交
    e.preventDefault()
  })
</script>

除了表单域跳转,还有链接跳转,阻止方法也是类似的。

解绑事件

有两种方式:on 事件方式和 addEventListener 事件方式

on 事件方式(L0事件移除解绑)

直接使用 null 覆盖偶就可以实现事件的解绑。

JavaScript 复制代码
// 绑定事件
btn.onclick = function () {
  alert('点击了')
}
// 解绑事件
btn.onclick = null

// 如果把解绑写在事件里 事件会只执行一次就再也不执行了
btns.onclick = function () {
  alert('你好呀')
  btn.onclick = null
}
addEventListener 事件方式(L2事件解绑)

如果使用 addEventListener 方式,必须使用:

removeEventListener(事件类型, 事件处理函数, [获取捕获或者冒泡阶段])

例如:

JavaScript 复制代码
function () {
  alert('点击了')
}
// 绑定事件
btn.addEventListener('click', fn)
// 解绑事件
btn.removeEventListener('click', fn)

注意:匿名函数无法被解绑!

补充

补充一:鼠标经过事件的区别

鼠标经过事件其实是有两种写法,两种没有太大差别,但是

  • mouseover 和 mouseout 会有冒泡效果
  • mouseenter 和 mouseleave 没有冒泡效果(推荐)
补充二:两种注册事件的区别
  • 传统 on 注册(L0)
    • 同一个对象,后面注册的事件会覆盖前面注册的事件(这里指同一个事件)
    • 直接使用 null 覆盖偶就可以实现事件的解绑
    • 都是冒泡阶段执行的
  • 事件监听注册(L2)
    • 语法:addEventListener(事件类型, 事件处理函数, 是否使用捕获机制)
    • 后面注册的事件捕获覆盖前面注册的事件(同一个事件)
    • 可以通过第三个参数去确认狮子啊冒泡或者捕获阶段执行
    • 必须使用removeEventListener(事件类型, 事件处理函数, [获取捕获或者冒泡阶段])解绑
    • 匿名函数无法被解绑

事件委托

其实事件冒泡并非只有弊端,有些时候,我们需要使用它来更方便地解决一些问题。

思考一下,如果我们同时给多个元素注册事件,比如给 ul 中的每个小 li 添加点击事件。我们通常是使用 for 循环来遍历元素注册事件。这样其实还挺麻烦的,有没有一种技巧,只注册一次就能完成以上效果呢?

答案是肯定的,这就需要回到一次我们的主题------事件委托上了。

事件委托是利用事件流的特征解决一些现实开发需求的知识技巧,主要的作用是提升程序效率。

事件委托的优点是:减少注册次数,可以提高程序性能。

原理 :利用事件冒泡的特点:

  • 给父元素注册事件。当我们触发子元素 的时候,会冒泡到父元素身上(我们没有给子元素注册点击事件),从而触发父元素的事件
  • 实现:事件对象.target.tagName可以获得真正触发事件的元素

我们举个例子来更好地理解一下事件委托:

html 复制代码
<ul>
  <li>第1个孩子</li>
  <li>第2个孩子</li>
  <li>第3个孩子</li>
  <li>第4个孩子</li>
  <li>第5个孩子</li>
  <p>我不需要变色</p>
</ul>
<script>
  // 点击每个小li 当前li 文字变为红色
  // 按照事件委托的方式  委托给父级,事件写到父级身上
  // 1. 获得父元素
  const ul = document.querySelector('ul')
  ul.addEventListener('click', function () {
    alert(11)
  })
</script>

利用事件流的特征,事件的的冒泡模式总是会将事件流向其父元素的,如果父元素监听了相同的事件类型,那么父元素的事件就会被触发并执行,正是利用这一特征,,我们不再需要使用复杂的 for 循环来实现效果。现在,我们只要点击任意一个小 li 就能弹出弹框。

如果,我们想实现点击某个 li,被点击的 li 就会变红色这一效果,怎么实现呢?

直接添加更改颜色的代码?

JavaScript 复制代码
const ul = document.querySelector('ul')
ul.addEventListener('click', function () {
  this.style.color = 'red'
})

运行后你会发现,当你点击任意一个小 li,全部的小 li 都变红色了。这和我们想要的效果相离。

还记得之前我们学到的事件对象e吗?在e中有一个属性叫target,它记录我们鼠标点击的那个对象。比如鼠标点击 ul 里的 li,target会记录 li。我们可以在代码中尝试查看 target,发现target 里有一个属性tagName,其实,这个 tagName 才是记录当前点击的元素的真正属性。我们可以借用这个属性来设置颜色状态。

JavaScript 复制代码
const ul = document.querySelector('ul')
ul.addEventListener('click', function (e) {
  if (e.target.tagName === 'LI') {  // 注意记录的元素格式是大写
    e.target.style.color = 'red'
  }
})
// e.target 相当于 当前所指的元素标签

很明显,这个实现方法比我们通过 for 循环来一次修改样式要效率很多!

其他事件

页面加载事件

加载外部资源(如图片、外联CSS和JavaScript等)加载完毕时触发的事件

使用场景:

  • 有些时候需要等页面资源全部处理完了做一些事情
  • 老代码喜欢把 script 写在 head 中,这时候直接找 dom 元素找不到(因为代码是自上而下执行的,因此会先执行 JavaScript 中的代码,此时 html 中的元素和样式 css 还没在页面中加载,导致无法获取)

有两种加载事件语法,其事件名各不同。

事件名:load

语法:监听页面所有资源加载完毕:

  • 给 window 添加 load 事件
javascript 复制代码
window.addEventListener('load', function() {
    // 执行的操作
})

比如:我们在页面中添加一个按钮,但是把 script 写在 head 后,此时 script 中的代码需要等待 button 加载后才能执行,因此我们要添加页面加载事件。

html 复制代码
<!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>
  <script>
    // 等待页面所有资源加载完毕,就回去执行回调函数
    window.addEventListener('load', function () {
    	const btn = document.querySelector('button')
    	btn.addEventListener('click', function () {
      	alert(11)
      })
    })
  </script>
</head>

<body>
  <button>点击</button>

</body>

</html>

注意:页面加载事件不仅可以监听整个页面资源加载完毕,也可以针对某个资源绑定 load 事件。

比如:页面中有很大像素的图片加载的比较慢,我们针也对这个图片专门去绑定一个事件

html 复制代码
<script>
  img.addEventListener('load', function () {
    // 等待图片加载完毕,再去执行里面的代码
  })
</script>
事件名:DOMContentLoaded

有时候,我们甚至可以只等到页面的 html 元素加载出来就可以做事件监听。当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像等完全加载。这样页面加载会更快一些,效率得到提高。

语法:监听页面 DOM 加载完毕:

  • 给 document 添加 DOMContentLoaded 事件
javascript 复制代码
document.addEventListener('DOMContentLoaded', function () {
  // 执行的操作
})

在上述案例中,我们可以这么做:

html 复制代码
<!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>
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      const btn = document.querySelector('button')
      btn.addEventListener('click', function () {
        alert(11)
      })
    })
  </script>
</head>

<body>
  <button>点击</button>

</body>

</html>

页面滚动事件

滚动条在滚动的时候持续触发的事件

使用场景:很多网页需要检测用户把页面滚动到某个区域后做一些处理,比如固定导航栏、返回顶部,比如页面滚动到某个部分,侧边栏也标记显示到该部分。

事件名:scroll

语法:监听整个页面滚动:

  • 给 window 或 document 添加 scroll 事件
javascript 复制代码
window.addEventListener('scroll', function() {
    // 执行的操作
})
  • 如果想监听某个元素内部滚动直接给那个元素加即可,同上页面加载。

我们来尝试一个案例,我们想要页面滚动一段距离,比如 100px,就让某个元素显示/隐藏,可以使用 scroll 来检测滚动距离。

在此之前,我们需要了解两个属性:scrollLeftscrollTop

功能:

  • 获取被卷去的大小
  • 获取元素内容往左、往上滚出去看不到的距离
  • 这两个值是可读写
  • 我们更常用scrollTop

我们尽量在 scroll 事件里去获取被卷去的距离

语法:

JavaScript 复制代码
div.addEventListener('scroll', function(){
  console.log(this.scrollTop)
})
获取位置

开发中,我们常检测页面滚动的距离,比如页面滚动 100px,就可以显示一个元素:

JavaScript 复制代码
window.addEventListener('scroll', function () {
	// document.documentElement 是 html 元素的获取方式
  // 这句话一定要写在函数里面
  const n = document.documentElement.scrollTop
  // n 数字型 不带单位
	console.log(n)
})

由于 scrollTop 是可读写的,所以是可以赋值的,这样打开页面后滚动条会自动跳转到固定位置:

javascript 复制代码
document.documentElement.scrollTop = 800
window.addEventListener('scroll', function () {
	// document.documentElement 是 html 元素的获取方式
  const n = document.documentElement.scrollTop
})

当我们想要实现返回顶部的效果,可以直接把scrollTop赋值为0即可。

页面尺寸事件

会在窗口尺寸改变的时候触发事件:

事件名:resize

javascript 复制代码
window.addEventListener('resize', function() {
    // 执行的代码
})

如果想检测屏幕宽度:

JavaScript 复制代码
window.addEventListener('resize', function() {
    let w = document.documentElement.clientWidth
    console.log(w)
})
获取元素宽高

可以通过属性:clientWidthclientHeight 获取元素的可见部分宽高(不包含边框、margin、滚动条等),但是包含 padding!注意,是只读属性。

元素尺寸与位置

使用场景:

  • 前面案例滚动多少距离,都是我们自己提前设定好的,但在实际开发中,最好是页面滚动到某个元素,就可以做某些事
  • 简单来说,就是通过 JS 的方式,得到元素在页面中的位置
  • 这样我们就可以做:页面滚动到这个位置,进行某些操作的效果了

获取位置方法一:尺寸offsetWidth/offsetHeight

获取宽高:

  • 获取元素的自身宽高、包含元素自身设置的宽高、padding、border

  • offsetWidthoffsetHeight(包含 border)

  • 获取出来的是数值,方便计算

  • 注意: 获取的是可视宽高, 如果盒子是隐藏 的,获取的结果是0

获取位置:

  • 获取元素距离自己定位父级元素的左、上距离
  • offsetLeftoffsetTop ,注意是只读属性

举个例子:我们在 div 盒子里面套一个小盒子 p,

html 复制代码
<!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>
    div {
      position: relative;
      width: 200px;
      height: 200px;
      background-color: pink;
      margin: 100px;
    }

    p {
      width: 100px;
      height: 100px;
      background-color: purple;
      margin: 50px;
    }
  </style>
</head>

<body>
  <div>
    <p></p>
  </div>
  <script>
    const div = document.querySelector('div')
    const p = document.querySelector('p')
    console.log(div.offsetLeft)
    // 检测盒子的位置  最近一级带有定位的祖先元素
    console.log(p.offsetLeft)
  </script>
</body>

</html>

可以看到,由于 div 盒子设置了定位,因此 p 的 offsetLeft 值是相对于 div 而言的,而 div 是相对 body 而言的:

获取位置方法二:getBoundingClientRect()

获取位置:

  • 通过element.getBoundingClientRect()返回元素的大小及其相对于视口的位置

我们在一个例子里体会一下:

html 复制代码
<!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>
    body {
      height: 2000px;
    }

    div {
      width: 200px;
      height: 200px;
      background-color: pink;
      margin: 100px;
    }
  </style>
</head>

<body>
  <div></div>
  <script>
    const div = document.querySelector('div')
    console.log(div.getBoundingClientRect())
  </script>
</body>

</html>

当我们滚动页面的时候,盒子div的位置会相对视口发生变化,因此由getBoundingClientRect()获得的信息会随着滚动而变化。

总结

相关推荐
云枫晖2 小时前
前端工程化实战:手把手教你构建项目脚手架
前端·前端工程化
醉方休2 小时前
开发一个完整的Electron应用程序
前端·javascript·electron
studyForMokey2 小时前
【Kotlin内联函数】
android·开发语言·kotlin
故作春风2 小时前
手把手实现一个前端 AI 编程助手:从 MCP 思想到 VS Code 插件实战
前端·人工智能
小虚竹2 小时前
Rust日志系统完全指南:从log门面库到env_logger实战
开发语言·后端·rust
星释2 小时前
Rust 练习册 8:链表实现与所有权管理
开发语言·链表·rust
今日说"法"2 小时前
Rust 日志级别与结构化日志:从调试到生产的日志策略
开发语言·后端·rust
-大头.2 小时前
Rust并发编程实战技巧
开发语言·后端·rust
不会算法的小灰2 小时前
Vue.js 基础教程:从入门到实践
前端·javascript·vue.js