# 事件

DOM 事件标准描述了事件传播的 3 个阶段:

  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。

  2. 目标阶段(Target phase)—— 事件到达目标元素。

  3. 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。

粟子

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    div{
      padding: 50px;
      background: #3eaf7c;
      text-align: center;
    }
    span{
      display: inline-block;
      width: 100px;
      height: 50px;
      background: #4a67de;
    }
  </style>
</head>
<body>
  <div>
    <span></span>
  </div>
</body>
<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => console.log(`Bubbling: ${elem.tagName}`));
  }
</script>
</html>

当我们点击元素 span时,控制台输出:

Bubbling: SPAN
Bubbling: DIV
Bubbling: BODY
Bubbling: HTML

发现显示的结果从里到外,也就是冒泡阶段,因为默认情况下事件处理的触发就是在冒泡阶段

为了查看捕获事件效果的,我们改下事件绑定的方式

elem.addEventListener("click", e => console.log(`Bubbling: ${elem.tagName}`), true);

此时我们再次点击元素 span 时,控制台输出:

Capturing: HTML
Capturing: BODY
Capturing: DIV
Capturing: SPAN

也可以将冒泡和捕获一起输出

elem.addEventListener("click", e => console.log(`Capturing: ${elem.tagName}`), true);
elem.addEventListener("click", e => console.log(`Bubbling: ${elem.tagName}`))

此时打印结果为:

Capturing: HTML
Capturing: BODY
Capturing: DIV
Capturing: SPAN
Bubbling: SPAN
Bubbling: DIV
Bubbling: BODY
Bubbling: HTML

# 阻止冒泡的捕获

e.stopPropagation(): 阻止捕获和冒泡阶段中当前事件的进一步传播。但是,它不能防止任何默认行为的发生; 例如,对链接的点击仍会被处理

  function handleDIV(e) {
    console.log('handleDIV')
  }
  function handleSPAN(e) {
    console.log('handleSPAN')
    e.stopPropagation()
  }
  function handleOther(e) {
    console.log('handle', e.currentTarget.tagName)
  }
  for(let elem of document.querySelectorAll('*')) {
    let fn = window['handle'+elem.tagName] ||handleOther
    elem.addEventListener("click", fn);
  }

此时点击元素 span 时,控制台只会输出:

handleSPAN

说明阻止了事件在冒泡阶段的传播

  function handleDIV(e) {
    console.log('handleDIV')
  }
  function handleSPAN(e) {
    console.log('handleSPAN')
  }
  function handleOther(e) {
    console.log('handle', e.currentTarget.tagName)
    e.stopPropagation()
  }
  for(let elem of document.querySelectorAll('*')) {
    let fn = window['handle'+elem.tagName] ||handleOther
    elem.addEventListener("click", fn, true);
  }

将粟子改成捕获的形式,此时点击元素 span 时,控制台只会输出:

handle HTML

说明阻止了事件在捕获阶段的传播

# 阻止默认行为

preventDefault()

如果此事件没有被显式处理,它默认的动作也不应该照常执行。此事件还是继续传播,除非碰到事件侦听器调用 stopPropagation()stopImmediatePropagation(),才停止传播

<a  href="www.baidu.com">www.baidu.com</a>
<script >
  function handleA(e) {
    console.log('handleA')
    e.preventDefault()
  }
</script>

此时点击 a 标签,将不会触发跳转行为

# target和currentTarget

  • currentTarget:表示当前绑定事件事件处理器的所在元素

  • target:表示触发事件元素

上文提到因为事件存在冒泡和捕获阶段,所以一个元素的绑定事件的触发不一定是当前这个元素

<body>
  <div>
    <span></span>
  </div>
</body>
<script>
  function handleDIV(e) {
    console.log('handleDIV-target', 'target:',  e.target.tagName, 'currentTarget', e.currentTarget.tagName)
  }
  function handleSPAN(e) {
    console.log('handleSPAN-target', 'target:',  e.target.tagName, 'currentTarget', e.currentTarget.tagName)
  }
  function handleOther(e) {
    console.log('handle', e.currentTarget.tagName, 'target:',  e.target.tagName, 'currentTarget', e.currentTarget.tagName)
  }
  for(let elem of document.querySelectorAll('*')) {
    let fn = window['handle'+elem.tagName] ||handleOther
    // elem.addEventListener("click", fn, true);
    elem.addEventListener("click", fn);
  }
</script>

点击 span 元素时,输出结果为:

handleSPAN-target target: SPAN currentTarget SPAN
handleDIV-target target: SPAN currentTarget DIV
handle BODY target: SPAN currentTarget BODY
handle HTML target: SPAN currentTarget HTML

因为我们点击的 span 元素,所以 target 都是 span 元素, currentTarget 为绑定事件所在的元素

# 事件委托

事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托(事件代理)。

使用事件委托可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理还可以实现事件的动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理

# 事件委托的特点

减少内存消耗

果有一个列表,列表之中有大量的列表项,需要在点击列表项的时候响应一个事件:

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>

如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件时再去匹配判断目标元素,所以事件委托可以减少大量的内存消耗,节约效率

动态绑定事件

给上述的例子中每个列表项都绑定事件,在很多时候,需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的,所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。

# 局限性

当然,事件委托也是有局限的。比如 focusblur 之类的事件没有事件冒泡机制,所以无法实现事件委托;mousemovemouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的