Skip to content

浏览器的事件机制 #19

Open
Open
@myLightLin

Description

@myLightLin

浏览器是多线程的,其中渲染线程负责页面 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

image

这个过程有点像递归,几乎所有主流浏览器都支持这种 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 的两个属性,分别是 targetcurrentTarget。那它们之间有什么区别呢?我们来浏览器里实验一下

<!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 标签,此时控制台打印如下:
image

可以看到,当我们点击 item1 这个 li 元素时,event.target 指的就是你当前点击的元素;而 event.currentTarget 指的是绑定事件处理程序的目标元素,也就是 ul 标签。

总结

  • 浏览器通过事件来支持 HTML 与 JS 交互
  • 事件流规定页面接收事件的顺序,有 事件冒泡事件捕获 两种
  • 对于 DOM 事件,分为三个阶段:捕获阶段目标阶段冒泡阶段
  • 了解事件委托的原理,以及 event.targetevent.currentTarget 的区别

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions