Description
浏览器是多线程的,其中渲染线程负责页面 UI 的渲染,生成 HTML 文档页面;而 JS 线程负责执行逻辑代码,那 JS 与 HTML 是怎么交互的?假设我们有一个 html 页面,里面有一个按钮:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button>点击我</button>
</body>
</html>
现在这个页面是静态的,我们如何实现点击按钮的时候,弹出一个提示文字呢,于是我们想到,给这个按钮绑定一个事件:
<button id="btn" onClick="alert('hello world')">点击我</button>
// 或者用 JS
<script type="text/javascript">
const btn = document.getElementById('btn')
btn.addEventListener('click', function() {
alert('hello world')
})
</script>
这就是 HTML 与 JS 的交互方式:通过 事件
。那现在问题又来了,我们上面只是一个简单的按钮,如果是一个 div 容器,里面包含了多个元素,有列表,有图片还有其它等等,那这时候事件是怎么传播的呢?答案是事件流。
事件流
事件流规定了页面接收事件的一个顺序,针对这个顺序问题,IE 开发团队和 Netscape 团队提出了完全相反的方案:IE 团队支持事件冒泡流,而 Netscape 团队支持事件捕获流。
- 事件冒泡:当点击一个元素的时候,先从这个元素开始触发事件,然后沿着 DOM 树一层层往上,每经过一个节点依次触发事件,直到最上的 document 对象。它的特点是自底向上。
- 事件捕获:与事件冒泡相反,当点击事件触发时,首先被 document 捕获,然后沿着 DOM 树依次向下传播,直至到达目标元素,也就是你实际点击的那个元素。它的特点是自顶向下。
DOM 事件流
上面这两家团队各自提出了对文档中事件传播的解决方案,后来 W3C 谁也不得罪,把这两者结合起来了,DOM Level 3 规定了事件传播的三个阶段:事件捕获、目标阶段和事件冒泡。具体过程如下:
- 点击事件触发时,首先从 document 文档捕获,然后沿着 DOM 树将事件一层层往下传播
- 这时到达目标元素,在目标元素这触发
- 接着沿着目标元素往上冒泡,最后回到 document
这个过程有点像递归,几乎所有主流浏览器都支持这种 DOM 事件机制。那上面的这个事件传播,有没办法阻止它呢?答案是通过 e.stopPropagation()
和 e.stopImmediatePropagation()
其中 e 是事件绑定回调的 event 参数,这两个方法的区别在于前者只会阻止冒泡和捕获,而后面这个方法还会额外阻止这个元素的其它事件发生,后者清除的更彻底。
另外,还有个 e.preventDefault()
这个方法是为了阻止默认事件的发生,比如你有一个 <a>
标签,默认自带超链接功能,你想阻止掉默认跳转,就可以使用这个方法来实现。
绑定处理事件
了解了事件机制后,接下来就是根据需要绑定事件,来达到我们的效果了。通常绑定事件有以下几种方式:
- 在 html 标签里直接绑定,比如上面的代码里
<button onClick=""></button>
- 通过 JS 绑定,比如上面代码里的
document.getElementById('btn').addEventListener('click', fn)
- 通过全局 document 对象,如
document.addEventListener('click', fn)
addEventListener
方法接收三个参数:
addEventListener(type, listener)
addEventListener(type, listener, options)
addEventListener(type, listener, useCapture)
第一第二个参数就是注册的事件以及对应的处理函数,主要是第三个参数,它可以是个 object,也可以是个布尔值。如果它是 object,它的可选值如下:
{
capture: false, // 是否使用捕获模式,默认为 false
once: false, // 是否只执行一次,默认为 false
passive: false, // 是否阻止 preventDefault 默认行为,默认为 false
signal: 指定 absort 方法来移除监听器
}
如果第三个参数传的是布尔值,则代表 useCapture,是否使用捕获模式
事件委托
日常工作中,我们应用 DOM 事件流最多的就是事件委托
了,它的基本原理就是在父元素绑定处理事件,然后利用冒泡机制来对子元素进行事件处理工作。
比如有个列表:
<ul id="ul">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
现在我们想对每个 li 元素绑定点击事件,跳转特定的详情页。我们当然不能一个个的在 li 标签上手写事件绑定,正确的做法是在父元素 ul 标签绑定一个事件处理程序,如下:
const ul = document.getElementById('ul')
ul.addEventListener('click', function(event) {
if (event.target.tagName.toLowerCase() === 'li') {
// 执行跳转操作
}
})
这里还要提到 event 的两个属性,分别是 target
和 currentTarget
。那它们之间有什么区别呢?我们来浏览器里实验一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="ul">
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
<script type="text/javascript">
const ul = document.getElementById('ul')
ul.addEventListener('click', function(event) {
if (event.target.tagName.toLowerCase() === 'li') {
console.log('event target', event.target)
console.log('event current target', event.currentTarget)
}
})
</script>
</body>
</html>
在浏览器里打开这个 html 文件,按住 F12 打开 console,然后随便点击一个 li 标签,此时控制台打印如下:
可以看到,当我们点击 item1 这个 li 元素时,event.target
指的就是你当前点击的元素;而 event.currentTarget
指的是绑定事件处理程序的目标元素,也就是 ul 标签。
总结
- 浏览器通过事件来支持 HTML 与 JS 交互
- 事件流规定页面接收事件的顺序,有
事件冒泡
和事件捕获
两种 - 对于 DOM 事件,分为三个阶段:
捕获阶段
,目标阶段
和冒泡阶段
- 了解事件委托的原理,以及
event.target
与event.currentTarget
的区别