认识Dom
(Document Object Model),W3C 制定的标准接口规范,是一种处理 HTML 和 XML 文件的标准 API。它把文档作为一个树形结构,树的每个结点表示了一个 HTML 标签或标签内的文本项.
DOM 树结构精确地描述了 HTML 文档中标签间的相互关联性。将 HTML 或 XML 文档转化为 DOM 树的过程称为解析(parse)
可以试着在浏览器把一个 DOM 节点输出来看一下:
let body = document.body;
for (var key in obj) {
console.log(`属性:${key},值:${body[key]}`)
}
这个节点有很多属性,其中 onclick
的默认值是 null
,当把 onclick
赋予一个函数,就可以作为一个事件函数存在。我们可以通过给对应的属性绑定事件来修改、添加和删除DOM 树的结点和内容。
DOM 元素的查找
通过 JavaScript 查找 DOM 元素有以下几种方法:
- 通过
id
查找 DOM 元素 - 通过标签名查找 DOM 元素
- 通过类名查找 DOM 元素
- 通过 CSS 选择器查找 DOM 元素
// 通过 id 查找 DOM 元素
let id = document.getElementById("id"); // 查找 id="id" 的元素
// 通过标签名查找 DOM 元素
let p = document.getElementsByTagName("p"); // 查找所有 <p> 元素
// 通过类名查找 DOM 元素
let cls = document.getElementsByClassName("cls"); // 返回包含 class="cls" 的所有元素的列表
// 通过 CSS 选择器查找 DOM 元素
let pCls = document.querySelectorAll("p.cls"); // 返回 class="cls" 的所有 <p> 元素列表
事件注册
我们一般使用 addEventListener
来注册事件,它接受三个参数:
- 处理的事件名称,如点击事件
click
; - 事件处理程序,即要绑定的函数体;
- 指定是在事件冒泡还是事件捕获阶段处理参数,可以是布尔值,也可以是对象
true
则作为捕获事件处理;false
则作为冒泡事件处理(默认)。
第三个参数是对象时,可以使用以下几个属性:
capture
:布尔值,和第三个参数作为布尔值时作用一样once
:布尔值,值为true
表示该回调只会调用一次,调用后会移除监听passive
:布尔值,表示永远不会调用preventDefault
一般来说,我们可以通过使用 stopPropagation
来阻止事件的进一步传播,即阻止事件冒泡。
// stopPropagation() 可以阻止捕获和冒泡
e.stopPropagation();
// stopImmediatePropagation() 也可以阻止捕获和冒泡,但主要作用是阻止监听同一事件的其他事件监听器被调用
e.stopImmediatePropagation();
事件触发三阶段
- 事件捕获阶段:
window
往事件触发处传播,遇到注册的捕获事件会触发 - 处于目标阶段:传播到事件触发处时触发注册的事件
- 事件冒泡阶段:从事件触发处往
window
传播,遇到注册的冒泡事件会触发
首先发生的是事件捕获为截获事件提供机会,然后的是实际的目标接收事件,最后一个阶段是事件冒泡阶段,可以在这个阶段对事件做出响应。 如图:
假设在 DOM 结构里面有 text
的这样一个标签,给这个标签绑定了一个点击事件,那么在点击这个标签的时候是怎么执行事件的呢?
- 首先是事件捕获阶段,会通过
window
、document
、body
、div
、text
这样的顺序一直往下捕获事件。 - 然后是处于目标阶段,到
text
标签处触发绑定的点击事件。 - 最后是事件冒泡阶段,事件是在冒泡阶段做出响应的。冒泡阶段通过
text
、div
、body
、document
、window
这样的顺序往上冒泡,假如在div
或者body
上面也绑定了对应的onclick
事件,那么会按顺序触发响应。
事件触发一般来说会按照上面的顺序进行,但是也会有特例,如果给一个 body
中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。
感谢@dayTimeAffect的指出,以上说法不够严谨,在火狐和360浏览器是按照注册的顺序执行,但是在谷歌上面是按照先捕获后冒泡的顺序执行的(测试了火狐、360、谷歌、Edge四款浏览器),也就是说这个注册的顺序不同在不同的浏览器会有不同的表现,要注意区分。所以建议注册按照先捕获再冒泡的顺序来。
// 经测试,在火狐(Firefox 81.0)、360(版本12.2)浏览器是按照注册顺序执行的
// 即先打印“冒泡事件”再打印“捕获事件”
// 在谷歌(Chrome 90.0)、Edge(和谷歌同内核)浏览器是按照先捕获再冒泡的顺序执行的,
// 即先打印“捕获事件”再打印“冒泡事件”
var el = document.getElementById('el');
// 第三个参数为 false 即为冒泡事件
el.addEventListener('click', event => {
console.log('冒泡事件')
}, false);
// 第三个参数为 true 即为捕获事件
el.addEventListener('click', event => {
console.log('捕获事件')
}, true)
事件代理
事件代理是指利用事件冒泡,只指定一个事件处理程序来管理某一类型的所有事件。
如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话可以注册在父节点上。
<ul id="proxy">
<li>主页</li>
<li>文章</li>
<li>公告</li>
<li>简介</li>
</ul>
事件代理:
let proxy = document.querySelector('#proxy')
proxy.addEventListener('click', (event) => {
let target = event.target; // 当前点击的元素
if (target.nodeName.toLowerCase() == 'li') {
console.log('click:' + target.innerHTML);
}
})
这种方式相较于直接给目标注册事件来说,有以下优点:
- 可以减少内存占用,减少事件注册
- 不需要给子节点注销事件
总结
- DOM 是一个树形结构,树的每个结点表示了一个 HTML 标签或标签内的文本项。
- 可以通过
addEventListener
来注册事件。 - 事件触发有三个阶段,分别是事件捕获阶段、处于目标阶段、事件冒泡阶段。
- 通过利用事件冒泡来实现事件代理,可以提高性能。