DOM事件和事件委托

1,335 阅读4分钟

【前言】

在网页端、移动端H5、小程序等各个终端环境的前端开发中随处可见事件的运用,可见事件机制的是前端这一块的重中之重。

【目录】

  一、从实例看事件传递

  二、事件传递

  三、事件添加

  四、取消冒泡

  五、事件委托

  六、阻止默认动作

  七、封装事件委托      八、自定义事件   

【正文】

一、从实例看事件传递

以下面这个普通的html文件为例

<!DOCTYPE html>
<html lang="en" onclick="handleClickHtml()">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body onclick="handleBodyClick()">
  <div id="div1" onclick="handleClick2()">
    <button id="button1" onclick="handleClick1()">handleClick1</button>
  </div>
  <script>
    function handleClick1(e) {
      console.log('click1')
    }
    function handleClick2(e) {
      console.log('click2')
    }
    function handleBodyClick(e) {
      console.log('body clicked')
    }
    function handleClickHtml(e) {
      console.log('html clicked')
    }
    document.addEventListener('click', e => {
      console.log('document clicked')
    })
  </script>
</body>
</html>

例子中给document、html、body、div、button都添加了点击事件,其执行结果如下:

二、事件传递

事件产生后,从window对象自上而下向目标节点传递,抵达目标节点后会沿着相反方向传递

事件捕获:由内向外找监听函数

事件冒泡:由外向内找监听函数

W3C为了同时让IE和景文满意,只能规定浏览器应该同时支持两种调用方式

三、事件添加

  • 事件绑定API
    • IE5:X.attachEvent('onclick',fn)//冒泡
    • 网景:X.addEventListener('cilck',fn)//捕获
    • W3C:X.addEventListener('cilck',fn,bool)
  • 如果bool不传或为falsy, 就让fn走冒泡
  • 如果booltrue, 就让fn走捕获

四、取消冒泡

看下面这个例子,可以通过stopPropagation来阻止事件继续往上冒泡,window、document、html上添加的点击事件均不会生效

<!-- 省略了部分代码 -->
<div id="div1">
  <button id="button1">button</button>
</div>
<script>
  var div1 = document.getElementById('div1')
  var button1 = document.getElementById('button1')
  div1.addEventListener('click', (e) => {
    console.log(e.currentTarget)
  }, false)
  button1.addEventListener('click', (e) => {
    console.log(e.currentTarget)
    e.stopPropagation() // 关键代码:阻止了click事件继续往上冒泡
  }, false)
    
  // 以下是监听html、document的点击事件
  function handleClickHtml(e) {
    console.log('html clicked')
  }
  document.addEventListener('click', e => {
    console.log('document clicked')
  })
</script>

点击button的打印结果如下:

五、事件委托

大批量事件监听性能问题考虑以下场景:

有个一个长消息列表实时接受新的消息,滚动到底部时加载更多消息,点击消息可进入回话窗口页面,消息左滑消息此条消息

问题:

如何给此消息列表添加左滑事件?

抽象来看这个问题就是如何高效的给大批量(甚至是无限量)节点添加事件,这里暂时不考虑vue、react等框架

性能问题原因

每注册一个事件监听监听都需要使用一定内存,同时在dom节点多了之后事件经过的捕获、冒泡阶段要处理事件传递也就慢了

事件委托

父节点嵌套的子节点的触发的事件会通过事件冒泡到达父节点,事件处理程序不直接绑定到子节点,统一由公共父节点进行事件监听。

e.target表示触发事件的元素,通过e.target可以判断具体响应那个元素的事件

e.currentTarget表示事件绑定的元素,在事件委托情况下指向同一个元素

采用事件委托改造本文例子

<!-- 省略了部分代码 -->
<div id="div1">
  <button id="button1">button</button>
</div>
<script>
  document.addEventListener('click', e => {
    // e.target:触发事件的元素
    // e.currentTarget:事件监听器绑定的元素
    // console.log(e.target, e.currentTarget)
    if (e.target.id === 'button1') {
      return console.log('button1 clicked')
    }
    if (e.target.id === 'div1') {
      return console.log('div1 clicked')
    }
    if (e.target.tagName === 'HTML') {
      return console.log('html clicked')
    }
    if (e.target.tagName === 'BODY') {
      return cosnole.log('body clicked')
    }
  })
</script>

依次点击html、div1、button1会依次打印结果:

六、阻止默认事件

浏览器的默认事件就是浏览器自己的行为,比如我们在点击 <a href="#"> 的时候,浏览器跳转到指定页面。还有,当我们滚动鼠标时页面会向下滚动,但我们按空格键和按方向键时页面也会向下滚动,为了更好的用户体验,这时我们就需要阻止这种行为的发生。

 function stopDefault( e ) { 
if ( e && e.preventDefault ){
     e.preventDefault(); //阻止默认浏览器动作(W3C) 
 }else {
    window.event.returnValue = false; //IE中阻止函数器默认动作的方式 
 }
return false; 
}

javascript的return false会阻止默认行为,只需要支持高版本浏览器的话,一句话即可。

obj.onclick = function (){ 
  return false; 
}

七、封装事件委托

//部分代码
on('click', '#div', 'button',() => {
    console.log('button 被点击了')
})
function on(eventType, element, selector, fn) {
    if(!(element instanceof Elemnet) {
        element = document.querySelector(element)
    }
    element.addEventListener(eventType, e => {
        let el = e.target
        while (!el.matches(selector)){
            if (element === el) {
                el = null
                break
            }
            el = el.parentNode
        }
        el && fn.call(el, e, el)
    })
    return element
}

八、自定义事件