前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript 事件机制

JavaScript 事件机制

作者头像
用户1065635
发布2019-03-21 11:14:57
8440
发布2019-03-21 11:14:57
举报
文章被收录于专栏:前端社区前端社区

通俗地来说, JavaScript 事件机制描述的是事件在 DOM 里面的传递顺序,以及我们可以对这些事件做出如何的响应。

假设我们具有一个 ul 元素,其包括很多 li 元素。当我们点击任何一个 li 时,其实我们也点击了 ul ,因为 ul 把所有的 li 元素给“包装”了。

简单范例

在接下来的博文中,我们通过以下范例对事件机制进行介绍。

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<body>
  <ul id="list">
    <li id="list_item">
      <a id="list_item_link" target="_blank" href="https://google.com">
        Google
      </a>
    </li>
  </ul>
</body>
</html>

三个阶段

JavaScript 事件触发有三个阶段。

  • CAPTURING_PHASE,即捕获阶段
  • AT_TARGET,即目标阶段
  • BUBBLING_PHASE,即冒泡阶段

我们可通过事件对象的 eventPhase 属性,得知事件处于哪个阶段。eventPhase 为一个正整数,其定义可在 Event interface 查阅到。

代码语言:javascript
复制
const unsigned short CAPTURING_PHASE = 1;
const unsigned short AT_TARGET       = 2;
const unsigned short BUBBLING_PHASE  = 3;

DOM 的事件在传播时,会从根节点开始往下传递到 target ,若注册了事件监听器,则监听器处于捕获阶段。。

target 就是触发事件的具体对象,这时注册在 target 上的事件监听器处于目标阶段。

最后,事件再往上从 target 一路逆向传递到根节点,若注册了事件监听器,则监听器处于冒泡阶段。

注册事件

通常我们使用 addEventListener 注册事件,该函数有一个 useCapture 参数,该参数接收一个布尔值,默认值为 false ,代表注册事件为冒泡事件。若想注册事件为捕获事件,则将 useCapture 设置为 true

target 和 currentTarget

在了解上述的事件传递的三个阶段后,我们来梳理事件对象中容易混淆的两个属性:targetcurrentTarget

  • target 是触发事件的某个具体的对象,只会出现在事件机制的目标阶段,即“谁触发了事件,谁就是 target ”。
  • currentTarget 是绑定事件的对象。

两个有用的结论

先捕获,再冒泡

代码语言:javascript
复制
const get = (id) => document.getElementById(id);
const $list = get('list');
const $list_item = get('list_item');
const $list_item_link = get('list_item_link');
  
// list 的捕获
$list.addEventListener('click', (e) => {
  console.log('list capturing', e.eventPhase);
}, true);
  
// list 的冒泡
$list.addEventListener('click', (e) => {
  console.log('list bubbling', e.eventPhase);
}, false);
  
// list_item 的捕获
$list_item.addEventListener('click', (e) => {
  console.log('list_item capturing', e.eventPhase);
}, true);
  
// list_item 的冒泡
$list_item.addEventListener('click', (e) => {
  console.log('list_item bubbling', e.eventPhase);
}, false);
  
// list_item_link 的捕获
$list_item_link.addEventListener('click', (e) => {
  console.log('list_item_link capturing', e.eventPhase);
}, true);
  
// list_item_link 的冒泡
$list_item_link.addEventListener('click', (e) => {
  console.log('list_item_link bubbling', e.eventPhase);
}, false);

在我们点击超链接后,console 输出以下结果:

代码语言:javascript
复制
list capturing
1
list_item capturing
1
list_item_link capturing
2
list_item_link bubbling
2
list_item bubbling
3
list bubbling
3

从实验中,我们得出这样的一个结论:先捕获,后冒泡

在 target 注册的监听器,不分捕获和冒泡

既然我们得出了“先捕获,后冒泡”的结论,那么无论 addEventListener 的注册顺序如何改变,最终效果应该是一样的。理想很丰满,现实很骨感。

我们将前面的实验代码更改一下:

代码语言:javascript
复制
const get = (id) => document.getElementById(id);
const $list = get('list');
const $list_item = get('list_item');
const $list_item_link = get('list_item_link');
  
// list 的捕获
$list.addEventListener('click', (e) => {
  console.log('list capturing', e.eventPhase);
}, true);
  
// list 的冒泡
$list.addEventListener('click', (e) => {
  console.log('list bubbling', e.eventPhase);
}, false);
  
// list_item 的捕获
$list_item.addEventListener('click', (e) => {
  console.log('list_item capturing', e.eventPhase);
}, true);
  
// list_item 的冒泡
$list_item.addEventListener('click', (e) => {
  console.log('list_item bubbling', e.eventPhase);
}, false);
  
// list_item_link 的冒泡
$list_item_link.addEventListener('click', (e) => {
  console.log('list_item_link bubbling', e.eventPhase);
}, false);
  
// list_item_link 的捕获
$list_item_link.addEventListener('click', (e) => {
  console.log('list_item_link capturing', e.eventPhase);
}, true);

再次点击超链接,console 输出以下结果:

代码语言:javascript
复制
list capturing
1
list_item capturing
1
list_item_link bubbling
2
list_item_link capturing
2
list_item bubbling
3
list bubbling
3

可以发现,list_item_link 先执行了在冒泡阶段的 listener ,随后才执行捕获阶段的 listener

原因是当事件传递到 target 时,不管 addEventListneruseCapturetruefalsee.eventPhase 均为 AT_TARGET ,自然就没有捕获与冒泡的区别,故执行顺序取决于 addEventListen 的注册顺序。

由上面的实验,我们得出第二个结论:在 target 注册的监听器,不分捕获和冒泡

取消事件传递

我们可以通过 e.stopPropagation 中断事件的向下或向上传递。

在前面的实验代码中,我们给 list 的捕获阶段监听器添加中断事件传播的方法。

代码语言:javascript
复制
// list 的捕获
$list.addEventListener('click', (e) => {
  console.log('list capturing');
  e.stopPropagation();
}, true);

则在点击超链接后,会输出以下结果:

代码语言:javascript
复制
list capturing

可见,事件传播被中断了,剩下的 listener 不能接收到事件。

不过,需要注意:stopPropagation 不能阻止同一节点的其他 listener 的执行

比如说:

代码语言:javascript
复制
// list 的捕获
$list.addEventListener('click', (e) => {
  console.log('list capturing');
  e.stopPropagation();
}, true);

// list 的捕获2
$list.addEventListener('click', (e) => {
  console.log('list capturing 2');
}, true);

则输出结果为:

代码语言:javascript
复制
list capturing
list capturing 2

若想让同一节点的其他 listener 不被执行,我们可以使用 e.stopImmediatePropagation 方法。

取消预设行为

我们可以使用 e.preventDefault 取消默认行为。

改写实验代码:

代码语言:javascript
复制
// list_item_link 的冒泡
$list_item_link.addEventListener('click', (e) => {
  e.preventDefault();
}, false);

这样,当我们点击超链接时,就不会执行原本的默认行为(新开分页或跳转)。很多人会将 e.stioPropagatione.preventDefault 混淆,事实上,e.preventDefault 与事件传递没有任何关系,并不会影响事件的向下或向上传播。

这里有个特别值得注意的地方,来自 W3C 。

Once preventDefault has been called it will remain in effect throughout the remainder of the event’s propagation.

上面这句话的意思是,只要调用了 preventDefault 方法,在之后传递新的事件里面也会有效果。

我们来看一个范例:

代码语言:javascript
复制
// list 的冒泡
$list.addEventListener('click', (e) => {
  console.log('list bubbling', e.eventPhase);
  e.preventDefault();
}, true);

结果是超链接的默认行为没有被执行,注意到:不管是在捕获阶段还是在冒泡阶段,只要使用了 preventDefault 方法,即可取消默认行为的执行

事件代理

当我们想在 ul 节点添加 1000 个 li ,若在每个 li 添加 eventListener ,则新建了 1000 个 function 。但通过事件传播机制,我们可以在 ul 注册 eventListener

这样的好处有亮点:

  • 节省内存
  • 不需要给子节点注销事件

参考资料

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简单范例
  • 三个阶段
  • 注册事件
  • target 和 currentTarget
  • 两个有用的结论
    • 先捕获,再冒泡
      • 在 target 注册的监听器,不分捕获和冒泡
      • 取消事件传递
      • 取消预设行为
      • 事件代理
      • 参考资料
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档