事件监听

结合 DOM 使用事件时,需要为 DOM 对象添加事件监听,等待事件发生(触发)时,便立即调用一个函数。

元素对象.addEventListener('事件类型', 要执行的函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<h3>事件监听</h3>
<p id="text">为 DOM 元素添加事件监听,等待事件发生,便立即执行一个函数。</p>
<button id="btn">点击改变文字颜色</button>
<script>
// 1. 获取 button 对应的 DOM 对象
const btn = document.querySelector('#btn')
// 2. 添加事件监听
btn.addEventListener('click', function () {
console.log('等待事件被触发...')
// 改变 p 标签的文字颜色
let text = document.getElementById('text')
text.style.color = 'red'
})
// 3. 只要用户点击了按钮,事件便触发了!!!
</script>
</body>

完成事件监听分成3个步骤:

  1. 获取 DOM 元素
  2. 通过 addEventListener 方法为 DOM 节点添加事件监听
  3. 等待事件触发,如用户点击了某个按钮时便会触发 click 事件类型
  4. 事件触发后,相对应的回调函数会被执行

事件监听三要素:

  1. 事件源:哪个DOM元素被事件触发,要获取DOM元素
  2. 事件类型:用什么方式触发,比如鼠标单击click、鼠标经过mouseover
  3. 事件调用的函数:要做什么

事件类型

鼠标事件

  1. click:鼠标点击
  2. mouseenter:鼠标经过
  3. mouseleave:鼠标离开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<h3>鼠标事件</h3>
<p>监听与鼠标相关的操作</p>
<hr>
<div class="box"></div>
<script>
// 需要事件监听的 DOM 元素
const box = document.querySelector('.box');
// 监听鼠标是移入当前 DOM 元素
box.addEventListener('mouseenter', function () {
// 修改文本内容
this.innerText = '鼠标移入了...';
// 修改光标的风格
this.style.cursor = 'move';
})
// 监听鼠标是移出当前 DOM 元素
box.addEventListener('mouseleave', function () {
// 修改文本内容
this.innerText = '鼠标移出了...';
})
</script>
</body>

焦点事件

  1. focus:获得焦点
  2. blur:失去焦点

键盘事件

  1. Keydown:键盘按下触发
  2. Keyup:键盘抬起触发

文本事件

  1. input:用户输入事件

事件对象

任意事件类型被触发时与事件相关的信息会被以对象的形式记录下来,我们称这个对象为事件对象

元素.addEventListener('click', function(e){ }) // e就是事件对象

事件回调函数的第1个参数即所谓的事件对象,通常习惯性的将这个对数命名为 eventev 、e 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<h3>事件对象</h3>
<p>任意事件类型被触发时与事件相关的信息会被以对象的形式记录下来,我们称这个对象为事件对象。</p>
<hr>
<div class="box"></div>
<script>
// 获取 .box 元素
const box = document.querySelector('.box')
// 添加事件监听
box.addEventListener('click', function (e) {
console.log('任意事件类型被触发后,相关信息会以对象形式被记录下来...');
// 事件回调函数的第1个参数即所谓的事件对象
console.log(e)
})
</script>
</body>

接下来简单看一下事件对象中包含了哪些有用的信息:

  1. ev.type:当前事件的类型
  2. ev.clientX/Y:光标相对浏览器窗口左上角的位置
  3. ev.offsetX/Y:光标相于当前 DOM 元素左上角的位置
  4. key:用户按下的键盘值

注:在事件回调函数内部通过 window.event 同样可以获取事件对象。

环境对象

环境对象指的是函数内部特殊的变量 this ,它代表着当前函数运行时所处的环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
// 声明函数
function sayHi() {
// this 是一个变量
console.log(this);
}
// 声明一个对象
let user = {
name: '张三',
sayHi: sayHi // 此处把 sayHi 函数,赋值给 sayHi 属性
}
let person = {
name: '李四',
sayHi: sayHi
}
// 直接调用
sayHi() // window
window.sayHi() // window
// 做为对象方法调用
user.sayHi() // user
person.sayHi() // person
</script>

结论:

  1. this 本质上是一个变量,数据类型为对象
  2. 函数的调用方式不同 this 变量的值也不同
  3. 【谁调用 this 就是谁】是判断 this 值的粗略规则
  4. 函数直接调用时实际上 window.sayHi() 所以 this 的值为 window

回调函数

如果将函数 A 做为参数传递给函数 B 时,我们称函数 A 为回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
// 声明 foo 函数
function foo(arg) {
console.log(arg);
}
// 普通的值做为参数
foo(10);
foo('hello world!');
foo(['html', 'css', 'javascript']);
function bar() {
console.log('函数也能当参数...');
}
// 函数也可以做为参数!!!!
foo(bar);
</script>

函数 bar 做参数传给了 foo 函数,bar 就是所谓的回调函数了!!!

我们回顾一下间歇函数 setInterval

1
2
3
4
5
6
7
<script>
function fn() {
console.log('我是回调函数...');
}
// 调用定时器
setInterval(fn, 1000);
</script>

fn 函数做为参数传给了 setInterval ,这便是回调函数的实际应用了,结合刚刚学习的函数表达式上述代码还有另一种更常见写法。

1
2
3
4
5
6
<script>
// 调用定时器,匿名函数做为参数
setInterval(function () {
console.log('我是回调函数...');
}, 1000);
</script>

结论:

  1. 回调函数本质还是函数,只不过把它当成参数使用
  2. 使用匿名函数作为回调函数比较常见

事件流

事件流指的是事件完整执行过程中的流动路径

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

event.png

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

简单的说,捕获阶段是从父到子的传导过程,冒泡阶段是从子向父的传导过程。

实际工作都是使用事件冒泡为主

事件捕获和冒泡

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<body>
<h3>事件流</h3>
<p>事件流是事件在执行时的底层机制,主要体现在父子盒子之间事件的执行上。</p>
<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');
// html 元素添加事件
document.documentElement.addEventListener('click', function () {
console.log('html...')
})
// body 元素添加事件
document.body.addEventListener('click', function () {
console.log('body...')
})
// 外层的盒子添加事件
outer.addEventListener('click', function () {
console.log('outer...')
})
// 中间的盒子添加事件
inner.addEventListener('click', function () {
console.log('inner...')
})
// 内层的盒子添加事件
child.addEventListener('click', function () {
console.log('child...')
})
</script>
</body>

执行上述代码后发现,当单击事件触发时,其祖先元素的单击事件也相继触发

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

image.png

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

如果事件是在冒泡阶段执行的,我们称为冒泡模式,它会先执行子盒子事件再去执行父盒子事件,默认是冒泡模式(如上图所示)。

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

将第三个参数改为true,即捕获模式

image.png

结论:

  1. addEventListener 第3个参数决定了事件是在捕获阶段触发还是在冒泡阶段触发
  2. addEventListener 第3个参数为 true 表示捕获阶段触发,false 表示冒泡阶段触发,默认值为 false
  3. 事件流只会在父子元素具有相同事件类型时才会产生影响
  4. 绝大部分场景都采用默认的冒泡模式(其中一个原因是早期 IE 不支持捕获)

阻止冒泡

阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。

事件对象.stopPropagation()

注意:前提是阻止事件对象冒泡需要拿到事件对象,在冒泡模式和捕获模式均有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<body>
<h3>阻止冒泡</h3>
<p>阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。</p>
<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...')
})
// 中间的盒子
inner.addEventListener('click', function (ev) {
console.log('inner...')
// 阻止事件冒泡
ev.stopPropagation()
})
// 内层的盒子
child.addEventListener('click', function (ev) {
console.log('child...')
// 借助事件对象,阻止事件向上冒泡
ev.stopPropagation()
})
</script>
</body>

结论:事件对象中的 ev.stopPropagation 方法,专门用来阻止事件冒泡

有时候需要阻止默认行为的发生,例如阻止链接跳转,表单域跳转等,就需要使用另一个

e.preventDefault()

可以阻止默认行为

解绑事件

on事件方式,直接使用null覆盖就可以实现事件的解绑

1
2
3
4
btn.onclick = function() {
alert('点击了!')
btn.onclick = null // 实现只能点击一次的效果
}

如果是使用了addEventListener()方式,必须使用:

1
2
3
4
5
6
7
function fn() {
alert('点击了!')
}
// 绑定事件
btn.addEventListener('click', fn)
// 解绑事件
btn.removeEventListener('click', fn)

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

鼠标经过事件:

  • mouseovermouseout 会有冒泡效果
  • mouseentermouseleave 没有冒泡效果 (推荐)

两种注册事件的区别:

image.png

事件委托

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

原理

  • 给父元素注册事件,当我们触发子元素的时候,会冒泡到父元素身上,从而触发父元素的事件

实现:

  • 事件对象.target.tagName可以获得真正触发事件的元素

大量的事件监听是比较耗费性能的,如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
// 假设页面中有 10000 个 button 元素
const buttons = document.querySelectorAll('table button');

// 用传统for循环实现,程序性能消耗大
for(let i = 0; i <= buttons.length; i++) {
// 为 10000 个 button 元素添加了事件
buttons[i].addEventListener('click', function () {
// 省略具体执行逻辑...
})
}

// 使用事件委托优化,假设上述的 10000 个 buttom 元素共同的祖先元素是 table
const parents = document.querySelector('table')
parents.addEventListener('click', function (ev) {
// console.log(ev.target);
// 只有 button 元素才会真正去执行逻辑
if(ev.target.tagName === 'BUTTON') {
// 执行的逻辑
}
})
</script>

事件对象中的属性 target 或 srcElement属性表示真正触发事件的元素,它是一个元素类型的节点。

优化过的代码只对祖先元素添加事件监听,相比对 10000 个元素添加事件监听执行效率要高许多!!!

其他事件

页面加载事件

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

有些时候需要等页面资源全部处理完了做一些事情

事件名load

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

  • window添加load事件

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

1
2
3
window.addEventListener('load', function() {
// 执行的操作
})

当初始的HTML文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,而无需等待样式表、图像等完全加载

事件名DOMContentLoaded

监听页面DOM加载完毕:

  • document添加DOMContentLoaded事件
1
2
3
document.addEventListener('DOMContentLoaded', function() {
// 执行的操作
})

元素滚动事件

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

很多网页需要检测用户把网页滚动到某个位置后做一些处理,比如固定导航栏、返回顶部等

事件名scroll

监听整个页面滚动:

  • windowdocument添加scroll事件
1
2
3
window.addEventListener('scroll', function() {
// 执行的操作
})
  • 监听某个元素的内部滚动,直接给某个元素添加即可

获取位置

image.png

  • scrollLeftscrollTop(属性)
    • 获取被卷去的大小
    • 获取元素内容往左、往上滚出去看不见的距离
    • 这两个值是可读写的
  • 尽量在scroll事件里面获取被卷去的距离
1
2
3
4
5
6
7
8
div.addEventListener('scroll', function() {
console.log(this.scrollTop) // 获取到顶部的距离
})
window.addEventListener('scroll', function() {
// document.documentElement 是HTML元素获取方式
const n = document.documentElement.scrollTop
console.log(n)
})

注意:document.documentElement HTML文档返回对象为HTML元素

滚动到指定坐标

scrollTo()方法可以将内容滚动到指定的坐标

语法元素.scrollTo(x, y)

1
window.scrollTo(0, 1000)

页面尺寸事件

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

事件名resize

1
2
3
4
5
window.addEventListener('resize', function() {
// 检测屏幕宽度
let w = document.documentElement.clientWidth
console.log(w)
})
  • 获取宽高
    • 获取元素的可见部分宽高(不包括边框、margin、滚动条等)
    • clientWidthclientHeight

元素尺寸与位置

  • 获取宽高

    • 获取元素的自身宽高、包含元素自身设置的宽高、padding、border
    • offsetWidthoffsetHeight
    • 获取出来的是数值,方便计算
    • 注意:获取的是可视宽高, 如果盒子是隐藏的,获取的结果是0
  • 获取位置:

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

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

属性 作用 说明
scrollLeft和scrollTop 被卷去的头部和左侧 配合页面滚动来用,可读写
clientWidth和clientHeight 获得元素宽度和高度 不包含border,margin,滚动条
用于js获取元素大小,只读属性
offserWidth和offsetHeight 获取元素宽度和高度 包含border,padding,滚动条等,只读
offsetLeft和offsetTop 获取元素距离自己定位父级元素的左、上距离 获取元素位置的时候使用,只读属性