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()获得的信息会随着滚动而变化。

总结

相关推荐
前端OnTheRun3 分钟前
如何禁用项目中的ESLint配置?
javascript·vue.js·eslint
csbysj20206 分钟前
XML 技术
开发语言
清晓粼溪7 分钟前
Java登录认证解决方案
java·开发语言
小徐Chao努力8 分钟前
Go语言核心知识点底层原理教程【变量、类型与常量】
开发语言·后端·golang
锥锋骚年9 分钟前
go语言异常处理方案
开发语言·后端·golang
沐知全栈开发9 分钟前
JSP 自动刷新技术详解
开发语言
Felixwb6669 分钟前
Python 爬虫框架设计:类封装与工程化实践
前端
前端无涯10 分钟前
APP 内嵌 H5 复制功能实现:从现代 API 到兼容兜底方案
javascript
广州华水科技10 分钟前
潜力榜单2025年单北斗GNSS位移监测高口碑产品推荐
前端
特立独行的猫a11 分钟前
C++使用Boost的Asio库优雅实现定时器与线程池工具类
开发语言·c++·线程池·定时器·boost·asio