DOM 事件进阶——事件流,事件对象和事件委托

三葉Leaves Author

事件监听

  • 解释:
    一旦行为或状态发生改变,便立即调用一个函数。

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

    1. 获取 DOM 元素

    2. 通过 addEventListener 方法为 DOM 节点添加事件监听

    3. 等待事件触发,如用户点击了某个按钮时便会触发 click 事件类型

    4. 事件触发后,相对应的回调函数会被执行

回调函数

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

两种绑定方式区别

使用 on 方式

Note

这种方式是公元 2000 年之前的普遍用法,处在 DOM 事件模型的 Level 0 阶段 ~ Level 1 阶段。L1 阶段并没有多少改进,只是将 L0 规范化。

1
2
3
element.onclick = function () {
alert('Hello');
};
  • 一个事件只能绑定一个处理器,新绑定会覆盖旧的。

  • 没有事件捕获(只有冒泡)。

  • 兼容性最好,但功能最弱。

使用事件监听器

这是 L2 阶段的用法。

1
2
3
element.addEventListener('click', function (e) {
console.log('点击了');
}, false); // 第三个参数是 useCapture 设置捕获
  • 支持事件 捕获阶段(capture)和冒泡阶段(bubble)

  • 提供更丰富的事件对象(如 e.targete.stopPropagation() 等)。

  • 跨浏览器更一致(IE9+ 开始兼容)。

L3 阶段(2004 年)

增加了第三个参数对象 { once, passive, capture }

1
2
3
element.addEventListener('scroll', handler, {
passive: true, // 告诉浏览器不会调用 preventDefault
});
  • once: true:事件监听器只执行一次,执行后自动移除。

  • passive: true:用于性能优化,比如滚动事件。

  • capture: true:明确处于捕获阶段。

事件解绑

对于 L0:

1
element.onclick = null;

对于 L2 和现代:

匿名函数无法解绑

1
element.removeEventListener('click', fn); 

常见的事件类型

鼠标事件

  • 单击 click,双击 dbclick
  • 移入 mouseenter & mouseover(冒泡),移出 mouseleave & mouseout(冒泡)
    mouseovermouseout 两个 O 会冒泡

键盘事件

  • keydown   键盘按下触发,keyup   键盘抬起触发

通常情况用 keyup 更合适一点,因为有些人喜欢按着不动。

焦点事件

  • focus  获得焦点,blur 失去焦点

文本框输入事件

  • input,在 textarea 这样的输入区变一下就会触发一次。常用来做输入区字符统计,例如:
1
2
3
4
5
6
7
function updateCountNum() {
let num = commentBox.value.length;
countNum.textContent = num;
}
commentBox.addEventListener("input", () => {
updateCountNum();
});

页面加载事件

load 事件

表示某个元素加载好以后触发。这个可以用于 window,表示整个页面,也可以用于各种媒体元素,比如 <img>, <script>, <link>, <iframe> 等,但是不能用于 document

🌟常见用法:

  • 等待图片加载好以后执行回调函数:
1
2
3
4
5
const img = document.querySelector("img");
img.addEventListener("load", () => {
console.log("图片加载完成");
});

  • 等所有元素加载好以后,执行回调函数:
这种写法并不推荐,更推荐用下面的 DOMContentLoaded 事件

1
2
3
window.addEventListener("load", () => {
console.log("页面所有资源(包括图片、CSS)都加载完了");
});

DOMContentLoaded 事件

等待 DOM 加载完成后,就触发此事件,而无需等待 CSS,图片以及其他外部资源。

什么是“DOM 加载完成”?

DOM 就绪(DOMContentLoaded):指的是浏览器已经完成 HTML 文档的解析,构建好了 DOM 树,此时你可以安全地通过 JS 操作 HTML 元素了。
举例:
对于这样的结构:

1
2
3
4
5
6
7
<body>
<h1>Hello</h1>
<img src="a.jpg">
<script>
// 当执行到这里时,DOMContentLoaded 可能已经触发
</script>
</body>

这段代码被浏览器解析完成后,可能生成这样的结构:

1
2
3
4
5
6
Document
└── html
└── body
├── h1
├── img
└── script

注意:此时 img 的 <img> 标签存在了,但图片 a.jpg可能在加载中,但我们已经能通过 JS 获取 img 元素了。

这个事件一般只用于 document 对象。

页面滚动事件

参见:
用 TOC 点击跳转案例学习滚动事件

事件对象

回调函数可以加一个参数,用于拿到元素的事件对象。可以在控制台打印一下这个对象,看看都有些什么信息:

1
2
3
4
commentBox.addEventListener("keyup", function (e) 
{
console.log(e)
});

值得注意的是,不同的事件类型的事件对象也是不同的
例如,我对 keyup 事件可以监听到按下的值,来完成“回车键+Ctrl”键发布评论的逻辑。这个对于 input 事件,则对象里没有 key 属性:

1
2
3
4
5
6
7
8
9
10
commentBox.addEventListener("keyup", function (e) 
{
if (e.key === "Enter" && e.ctrlKey === true) {
const content = commentBox.value;
commentBox.value = "";
inactiveCommentBox();
updateCountNum();
console.log("评论内容是" + content);
}
});

环境对象

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

  1. this 本质上是一个变量,数据类型为对象
  2. 函数的调用方式不同 this 变量的值也不同
  3. 【谁调用 this 就是谁】是判断 this 值的粗略规则

事件流

事件捕获和事件冒泡

  1. addEventListener 第3个参数决定了事件是在捕获阶段触发还是在冒泡阶段触发。所以捕获和冒泡只会触发其中一个。

  2. addEventListener 第3个参数为  true 表示捕获阶段触发,false 表示冒泡阶段触发,默认值false

1
2
3
outer.addEventListener('click', function () {
console.log('outer...')
}, true) // true 表示在捕获阶段执行事件。默认为 fales
  1. 事件流只会在父子元素具有相同事件类型时才会产生影响

  2. 绝大部分场景都采用默认的冒泡模式(其中一个原因是早期 IE 不支持捕获,也就是 Level0 的 onclick)

事件委托

作用

利用事件委托,可以避免给每一个子元素都添加监听器,而是给父元素加一个监听器。当子元素触发了事件的时候,会冒泡到父元素,父元素可以利用 e.target 确认出是哪一个子元素触发了事件。

  • 一个小案例:
    鼠标进入 ul 的 tab 栏,判断到底是在哪一个 li ,来显示对应内容:

技巧:筛选指定元素

就拿上面那个案例说,在 ul 里,除了点到 li,有可能鼠标还会点到 ul 内,li 外的空白区域。
亦或者在一些父元素内,仅仅希望某些子元素被触发。

我们知道,e.target 是一个 DOM 元素(即事件实际触发的目标节点),它是一个 Element 对象,因此具有所有 DOM 元素的属性和方法,比如标签名。

通过 tagName

我们可以通过这种方法筛选掉除了 a 标签以外的元素触发的事件:

1
2
3
4
5
6
7
if (e.target.tagName === "A") {
// 处理标签栏
...

// 处理内容区域
...
}
tagName 返回的是大写的字符串。

常见的 DOM 元素的属性和方法,我们可见:
DOM 元素的常见属性和方法

更现代化的做法是用这几种:
matches()closest()dataset

closest() 向祖先查找

closest() 方法返回当前元素或最近的(祖先)元素中,匹配选择器的第一个元素(如果没有,返回 null

1
2
// 返回找到的元素,或者 NULL
element.closest(selector)
matches() 精确匹配选择题

matches() 用于判断某个元素是否和指定的 CSS 选择器匹配。

1
2
// 返回布尔值
element.matches(selector)

阻断事件流

使用 stopPropagation() 方法可以在事件流的任意阶段阻断流动
该方法应该由事件对象调用。

1
2
3
4
document.addEventListener('click', function (e) {
alert('我是爷爷-捕获')
e.stopPropagation()
}, true)

阻止默认行为

同样的,还可以给事件对象添加 preventDefault() 方法,来阻止其的一些默认行为。常见的做法:提交表单前进行合法性校验。

下面这个案例,会阻止超链接元素的默认点击跳转行为,从而避免你用上某款低质量搜索引擎。

1
2
3
4
5
6
7
8
<a href="http://www.baidu.com" style="font-size: large">百度一下</a>
<script>
const a = document.querySelector("a");
a.addEventListener("click", function (e) {
e.preventDefault();
a.textContent = '0 人想用百度'
});
</script>
  • 标题: DOM 事件进阶——事件流,事件对象和事件委托
  • 作者: 三葉Leaves
  • 创建于 : 2025-05-20 00:00:00
  • 更新于 : 2025-05-31 13:43:53
  • 链接: https://blog.oksanye.com/d948da33c49e/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论