DOM事件和事件委托

485 阅读6分钟

DOM事件

什么是DOM事件

DOM事件不是JavaScript的功能,而是浏览器产生的,DOM使得JavaScript有能力对HTML上的事件做出反应,包括鼠标点击事件、滚动条滚动事件、页面内容变化等。

监听函数

DOM模型如何对事件做出响应?

浏览器的DOM事件模型通过监听函数(listener)对事件做出反应,事件发生后,如果有监听器监听到了这个事件,就执行相应的监听函数。

监听函数的绑定方法

通过HTML的属性绑定

HTML允许在元素的属性中定义监听函数。

<body onload="fn()"><div onclick="console.log('触发点击事件')">

元素的事件监听属性都是 on 加上事件名,使用这种方法绑定的监听函数,只会在冒泡阶段触发

<div onclick="console.log(2)">
  <button onclick="console.log(1)">点击</button>
</div>

因为通过元素属性绑定的监听函数只在冒泡阶段触发,所以点击button,先执行内部的console,再执行父元素的console。

console:

1

2

通过元素节点的事件属性绑定

window.onload = doSomething  //这里不需要写成doSomething(),只需要写出函数名即可,不像HTML属性需要给出完整的代码

div.onclick = function(e){
    console.log('触发点击事件')
}

这种方法绑定的监听函数也只在冒泡阶段触发

通过addEventListener方法绑定

window.addEventListener('load,doSomething,false')

所有DOM节点都有addEventListener方法,该方法接受三个参数。

  • type:事件名称,大小写敏感。
  • listener:监听函数。事件发生时,会调用该监听函数。
  • useCapture:布尔值,如果设为true,表示监听函数将在捕获阶段(capture)触发。该参数可选,默认值为false(监听函数只在冒泡阶段被触发)。 用法实例:
function hello() {
  console.log('Hello world');
}

var button = document.getElementById('btn');
button.addEventListener('click', hello, false);

事件绑定小结

通过HTML属性绑定违反了HTML和JavaScript代码分离的理念,不建议使用。通过元素节点的属性绑定缺点在于同一个事件只能绑定一个监听函数,比如定义两次onclick属性,后一次会覆盖前一次,也不推荐使用。推荐使用addEventListener函数来绑定监听函数,因为:

  • 同一个事件可以绑定多个监听函数
  • 能够手动指定在捕获阶段还是冒泡阶段触发监听函数

事件的传播

事件捕获和事件冒泡

事件发生后会在子元素和父元素之间传播,我们将这个过程分为三个阶段,第一个阶段是从window对象到目标节点(从外到内)寻找监听函数,称为“捕获阶段”;第二个阶段是事件触发阶段;第三个阶段是从目标节点到window对象(从内到外)寻找监听函数,称为“冒泡阶段”。

W3C发布了标准,规定浏览器应该同时支持两种调用顺序,首先按事件捕获路径寻找监听函数,然后按事件冒泡路径寻找监听函数。

事件冒泡可以被取消

捕获不可以取消,但是冒泡可以。

e.stopPropagation()可中断冒泡,一般用于封装某些独立的组件。

事件默认行为

事件默认行为包括:

  • a标签的href自动跳转
  • type=submit默认提交表单
  • 其他浏览器默认行为 当我们不希望这些事件的默认行为执行,比如不想在点击链接的时候跳转到链接地址,可以用preventDefault()
<a href="http://baid.com">baidu</a>
<script>
  document.querySelector('a').onclick= function(e){
    //阻止默认事件
    e.preventDefault()
    //点击时不跳转到链接地址,而是在控制台打印链接
    console.log(this.href)
    if(/baidu.com/.test(this.href)){
      location.href = this.href
    }
  }
</script>

不可阻止的事件默认行为

事件具有以下属性:

  • Bubbles 表示是否冒泡
  • Cancelable 表示是否可以阻止事件的默认行为 如果事件的Cancelable属性为NO,则该事件的默认动作是不可以阻止的。比如scroll事件,阻止scroll的默认动作(滚动)没用,因为先有滚动才有滚动事件,要阻止滚动只能阻止wheel(鼠标滚轮事件)和touchstart(手机触屏事件)的默认动作,但是滚动条还能用,也可以用CSS让滚动条width:0。用CSS的overflow:hidden也可以直接取消滚动条,但此时JS依然可以修改scrollTop。

事件委托

如果div里面的button有很多个,每个button绑定一个事件(button.addEventListener())将会浪费大量的内存空间,DOM操作速度也很慢,我们可以将监听绑定在祖先元素div上,委托div帮我们判断用户操作的对象是不是button,是则执行对应的事件。我们把这种将监听函数绑定在目标元素的祖先元素上的设计方法称为事件委托。

div.addEventListener('click',(e)=>{
	const t=e.target
  if(t.tagName.toLowerCase()==='button'){
  console.log('button被click')
  }
})

原理

事件委托是利用事件的冒泡原理来实现的。比如有这么一个节点树,div>ul>li>a,给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。

优点

  • 节省内存
  • 减少DOM操作,提高运行速度
  • 可以监听动态元素(当前还不存在的元素)

封装事件委托

通过把事件委托的代码封装成共用函数,我们可以在任意元素的任意后代上绑定任意事件的监听函数,大大节约了代码量。

用法举例: on('click','#testDiv','li','fn'),当用户点击#testDiv里的li元素时调用fn函数。

这个公用函数接受四个参数:

  • [string] eventType 事件类型
  • [string] element 代理绑定的目标对象,也就是用户当前操作元素的某个父级对象
  • [string] seletor 有事件需要被代理的后代元素
  • [Function] fn 事件被监听到发生时需要响应的函数
on('click','#testDiv','li',fn)
//当用户点击#testDiv里的li元素,调用fn函数。
function on(eventType,element,seletor,fn){
    if(!(element instanceof Element)){
    element = document.querySelector(element)
}
element.addEventListener(eventType,(e)=>{
   const t = e.target
    if(t.matches(selector)){
    fn(e)
     }
  })
}

思路

给祖先元素添加监听,判断当前的target是否满足selector,满足就执行fn,不满足就跳过。

存在的问题

如果当button里还有span元素时,target由button变为span,t.matches(selector)值为假,所以事件监听会失效。

事件委托封装改进

function(eventType,element, selector, fn) {
    if(!(element instanceof Element)){
    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
  },

这样就不会出现上述问题,首先判断元素当前操作的元素是否跟button匹配,如果不匹配就去判断它的父元素是否跟button匹配,如果找到了匹配的父元素就调用fn,如果一直找到顶层元素(element)仍未找到则流程结束。