JavaScript | 事件对象—事件流、常用属性

在JavaScript中经常可以看到事件对象的使用,所以打算写一篇博客来记录下学习此知识点的过程。

什么是事件对象?

首先我们需要了解一下事件对象是什么,先来看下MDN给出的定义

Event 接口表示在 DOM 中出现的事件。

一些事件是由用户触发的,例如鼠标或键盘事件;而其他事件常由 API 生成,例如指示动画已经完成运行的事件,视频已被暂停等等。事件也可以通过脚本代码触发,例如对元素调用 HTMLElement.click() 方法,或者定义一些自定义事件,再使用 EventTarget.dispatchEvent() 方法将自定义事件派发往指定的目标(target)。—— MDN-Event

可以看到事件对象是一个对象,而这个对象记录了事件触发的时候的相关信息,比如:

  • 触发鼠标点击事件的时候,事件对象记录了鼠标的位置相关的信息
  • 触发键盘事件的时候,事件对象会记录下用户按下的对应的按键的相关信息

如何使用(获取)事件对象?

通过DOM LV2的方式添加绑定事件的时候,回调函数的第一个参数就是事件对象,一般可以将其命名为eevent

1
2
3
document.querySelector('#test', function(e){
  console.log(e);
});

事件流

事件流描述了页面接收事件的顺序。——《JavaScript高级程序设计》

假如页面上有一个元素,该元素触发事件的时候,首先水发生事件捕获,然后才是事件冒泡

截图

事件冒泡

当一个元素触发事件后,会依次向上调用所有父级元素的同名事件

截图

事件冒泡是默认存在的

验证:

比如现在页面上有这几个元素,对应的CSS以及HTML如下

CSS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#test1 {
  width: 600px;
  height: 600px;
  background-color: red;
}

#test2 {
  width: 400px;
  height: 400px;
  background-color: yellow;
}

#test3{
  width: 200px;
  height: 200px;
  background-color: blue;
}

HTML:

1
2
3
4
5
<div id="test1">
  <div id="test2">
    <div id="test3"></div>
  </div>
</div>

截图

现在给分别给这几个元素添加点击事件

JS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let test1 = document.querySelector('#test1');
let test2 = document.querySelector('#test2');
let test3 = document.querySelector('#test3');
test1.addEventListener('click',function(){
  console.log('test1');
});
test2.addEventListener('click',function(){
  console.log('test2');
});
test3.addEventListener('click',function(){
  console.log('test3');
});

此时点击蓝色的部分(div#test3),就会依次输出test3test2test1的消息。

如果点击黄色的部分(div#test2),就会依次输出test2test1的消息。

如果点击红色的部分(div#test1),就会只输出test1的消息。

可以看到,结果符合事件冒泡的顺序div#test3->div#test2->div#test1(从里到外)。

事件捕获

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

截图

注:

  • 实际上,所有浏览器都是从 window 对象开始捕获事件,而 DOM2 Events 规范规定的是从 document 开始

  • 由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情 况下可以使用事件捕获。

    相关内容详细请见——《JavaScript高级程序设计》

事件捕获需要写对应的代码才能开启,如何开启事件捕获如下:

1
document.querySelector('#test1').addEventListener(eventType, callBack, useCapture)

说明:

  • 参数3useCapture接受一个布尔值,代表是否开启事件捕获
  • 参数3为true的时候代表开启事件捕获(默认状态为false

❕注意:默认DOM 0级,也就是element.onclick = function(){}的写法,只有冒泡没有捕获。

验证:

针对刚才冒泡的js代码做如下修改,开启事件捕获

1
2
3
4
5
6
7
8
9
test1.addEventListener('click',function(){
  console.log('test1');
}, true);
test2.addEventListener('click',function(){
  console.log('test2');
}, true);
test3.addEventListener('click',function(){
  console.log('test3');
}, true);

此时点击蓝色的部分(div#test3),就会依次输出test1test2test3的消息。

如果点击黄色的部分(div#test2),就会依次输出test1test2的消息。

如果点击红色的部分(div#test1),就会只输出test1的消息。

可以看到,结果符合事件捕获的顺序div#test1->div#test2->div#test3(从外到里)。

阻止停止冒泡,捕获

如果只想把对应的事件限制在子元素内,不想触发父级的事件,那么旧需要阻止事件流动。

通过e.stopPropagation()方法,就可以阻止事件冒泡

❕注:不推荐使用event.cancelBubble,该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。—— MDN-Event.cancelBubble

比如对于刚才的案例,如果指向要点击事件的触发只限制在子元素内,比如只想要div#test3点击的时候,只输出test3,那就可以加上这个方法。

对于事件捕获(不常用):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
test1.addEventListener('click',function(e){
  console.log('test1');
  e.stopPropagation();
}, true);
test2.addEventListener('click',function(e){
  // console.log('test2');
}, true);
test3.addEventListener('click',function(e){
  console.log('test3');
  // e.stopPropagation();
}, true);

加上这个方法后不管点击哪里,都只会输出test1了。

对于事件冒泡:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
test1.addEventListener('click',function(e){
  console.log('test1');
});
test2.addEventListener('click',function(e){
  console.log('test2');
});
test3.addEventListener('click',function(e){
  console.log('test3');
  e.stopPropagation();
});

现在点击div#test3就只会输出test3了。

阻止事件默认行为

如果不想要a标签点击直接跳转,表单点击不直接提交可以使用下面的方法

语法:

e.preventDefault()

验证:

HTML:

1
2
3
4
<form action="https://cn.bing.com/" method="post">
  <button>click</button>
</form>
<a href="https://cn.bing.com/" script="preventDefault()">link</a>

JS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let btn = document.querySelector('button');
let a = document.querySelector('a');
btn.addEventListener('click', function(e){
  // 阻止表单提交
  e.preventDefault();
});
a.addEventListener('click', function(e){
  // 阻止链接点击默认跳转
  e.preventDefault();
})

事件代理(委托)

可以利用事件冒泡的特性,可以只使用一个事件处理程序来管理一种类型的事件。——《JavaScript高级程序设计》

给所有元素共同的祖先节点添加一个事件处理程序(可以提高性能)

使用:通过事件对象的target属性(e.target),可以得到事件真正的触发者。

使用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<style>
  ul .active {
    color: red;
    border: 1px solid rgb(255, 28, 28);
  }
</style>
<div class="header">
  <ul>
    <li>第1个</li>
    <li>第2个</li>
    <li>第3个</li>
    <li>第4个</li>
    <li>第5个</li>
    <li>第6个</li>
  </ul>
</div>

比如页面上有这样一个tag栏,想要点击的时候对应的tag栏变色的效果,这个时候正常情况下可以使用,for循环的形式依次为每个li元素添加点击事件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 获取所有li元素组成的NodeList
let lis = document.querySelectorAll('li');
// 给每个NodeList添加点击事件
lis.forEach((item) => {
  item.addEventListener('click', function(){
    // 去除页面上已经激活的li样式 
    if (document.querySelector('.active')) {
      document.querySelector('.active').classList.remove('active');
    }
    // 激活当前li的样式
    this.classList.add('active');
  });
});

同样的使用事件代理也可以解决问题,并且可以提高网页性能,不需要在使用循环的形式来给每个元素做绑定事件了,对应的JS代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 事件代理
let ul = document.querySelector('ul');
ul.addEventListener('click', function (e) {
  // 如果点击的目标元素是li标签则进行样式修改
  if (e.target.tagName == 'LI') {
      // 去除页面上已经激活的li样式 
      if (document.querySelector('.active')) {
        document.querySelector('.active').classList.remove('active');
      }
      // 激活当前li的样式
      e.target.classList.add('active');
  }
});

🤖了解e.currentTarget返回事件的监听者(事件绑定的对象)。

事件对象中的常用属性

获取事件事件触发类型

  • type:获取当前触发的事件的类型(只读)

获取当前坐标的属性(点击事件)

  • offsetX/offsetY:返回的是点击时候相对点击的元素左上角的相对位置(包含外边距)的坐标。
  • pageX/pageY:返回的是整个文档的坐标,如果此时页面比较长可以获取文档中鼠标的坐标。
  • clientX/clientY:返回的是鼠标点击的时候光标浏览器可视范围相对左上角的坐标。
  • screenX/screenY:返回的是鼠标点击的时候光标相对屏幕左上角的坐标。
  • layerX/layerY:往上找有定位属性的父元素的左上角(自身有定位属性的话就是相对于自身),都没有的话,就是相对于body的左上角的坐标。

PS:有关于这块的内容可以参考笔者写的另一篇文章——> 链接在这里

获取按键的属性(键盘事件)

  • key:获取用户按下的物理按键的值(只读)
  • keyCode(不建议使用):返回按键对应的编号。(注:该功能已从Web标准中删除,尽管一些浏览器可能仍然支持它,但它正在被丢弃。)

target属性与currentTarget属性

  • target:返回事件真正的触发者
  • currentTarget:返回事件的监听者(事件绑定的对象)

eventPhase属性

  • eventPhase:表示事件流当前处于哪一个阶段。
    返回值 含义
    0 此时没有事件在处理
    1 事件处于捕获阶段
    2 事件已经到达触发者
    3 事件处于冒泡阶段

测试:

1、事件冒泡

HTML:

1
<div id="test3"></div>

CSS:

1
2
3
4
5
#test3{
  width: 200px;
  height: 200px;
  background-color: blue;
}

JS:

1
2
3
4
5
6
7
let test3 = document.querySelector('#test3');
test3.addEventListener('click',function(e){
  console.log('div e.eventPhase:' + e.eventPhase);
});
document.addEventListener('click', function(e){
  console.log('doc eventPhase:' + e.eventPhase);
});

点击div#test3依次输出

  • div e.eventPhase:2
  • doc eventPhase:3

2、事件捕获

对上面的代码做一点修改

1
2
3
4
5
6
7
let test3 = document.querySelector('#test3');
test3.addEventListener('click',function(e){
  console.log('div e.eventPhase:' + e.eventPhase);
});
document.addEventListener('click', function(e){
  console.log('doc eventPhase:' + e.eventPhase);
}, true);

点击div#test3依次输出

  • doc eventPhase: 1

  • div e.eventPhase: 2

button属性

  • e.buttonMouseEvent.button 是只读属性,它返回一个值,代表用户按下并触发了事件的鼠标按键。
返回值 含义
0 鼠标左键
1 鼠标中键
2 鼠标右键

测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let test3 = document.querySelector('#test3');
test3.addEventListener('mouseup',function(e){
  if (e.button == 2) {
    console.log('click the right button');
  } else if (e.button == 0){
    console.log('click the left button');
  } else if (e.button == 1){
    console.log('click the middle button');
  } else {
    console.log('????');
  }
});

参考文献