day14 事件捕获、冒泡与委托

148 阅读6分钟

每日一句

You're in control of your own life!

释义:你的人生你作主!

概念理解

  1. DOM 即 文档对象模型。

文档对象模型是一种与编程语言及平台无关的API(Application programming Interface),借助于它,程序能够动态地访问和修改文档内容、结构或显示样式。

  1. DOM0,DOM2,DOM3

W3C协会早在1988年就开始了DOM标准的制定,W3C DOM标准可以分为DOM1,DOM2,DOM3三个版本。 DOM1级主要定义的是HTML和XML文档的底层结构。

  1. DOM0怎么来的?

其实是没有的,叫的人多了就有了DOM0, 在1998 年 10 月 DOM1级规范成为 W3C 的推荐标准,在此之前的实现我们就习惯称为DOM0级,其实本是没有这个标准的。

DOM0就是直接通过 onclick写在html里面的事件;

DOM2是通过addEventListener绑定的事件, 还有IE下的DOM2事件通过attachEvent绑定;

DOM3是一些新的事件;

DOM2级和DOM3级的目的在于扩展DOM API,以满足操作XML的所有需求,同时提供更好的错误处理及特性检测能力。

事件流描述的是从页面中接受事件的顺序。

IE和Netscape开发团队居然提出了两个截然相反的事件流概念。

  • IE的事件流是事件冒泡流

  • Netscape的事件流是事件捕获流

而addEventLister的第三个参数时支持冒泡与捕获

以下摘自[MDN]:

target.addEventListener(type, listener, options);
target.addEventListener(type, listener, useCapture); // 捕获true,冒泡false 默认为false.
target.addEventListener(type, listener, useCapture, wantsUntrusted ); // Gecko/Mozilla only true , 则事件处理程序会接收网页自定义的事件。此参数只适用于 Gecko(chrome的默认值为true,其他常规网页的默认值为false),主要用于附加组件的代码和浏览器本身

事件冒泡

事件的传播为:从事件开始的具体元素,一层层往上传播,直到window对象。

现代浏览器都支持事件冒泡,IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。

事件捕获

事件的传播为:即从上至下,从document逐级向下传播到目标元素。

IE9、Firefox、Chrome和Safari目前也支持这种事件流模型,但是有些老版本的浏览器不支持,所以很少人使用事件捕获,而是用事件冒泡的多一点。

事件委托(代理)

传统的事件处理中,需要为每个元素添加事件处理器。js事件委托(代理)则是一种简单有效的技巧,通过它可以把事件处理器添加到一个父级元素上,从而避免把事件处理器添加到多个子级元素上。

每个函数都是对象,都会占用内存,内存中的对象越多,性能就越差。对事件处理程序过多问题的解决方案就是事件委托。

事件委托利用事件冒泡,只指定一个事件处理程序即可,就可以管理某一个类型的所有事件。

使用场景

如果你想要在大量子元素(包括动态添加的)中单击任何一个就可以运行一段代码,这个时候可以把事件监听器设置在父节点上。

实现方式 jquery 中的 on

jquery在1.7的版本之后,最流行的事件监听方法是$(元素).on(事件名,执行函数),它还有一种事件委托的写法$(委托给哪个元素).on(事件名,被委托的元素,执行函数)

手写事件委托demo

//错误版(bug在于如果用户点击的是li里边的span,就没法触发fn)
ul.addEventListener('click', function(e)){
    if(e.target.tagName.toLowerCase() === 'li'){
        fn();
    }
}
//高级版
function delegate(element,eventType,selector,fn){
    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)
    })
}

优点:1.减少内存消耗; 2.可动态绑定事件(可以监听动态元素或不存在的元素)

局限性:

  • focus、blur本身没有冒泡机制,就不能委托;

  • mousemove、mouseout 虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的;

小结:

  • 适合用事件委托的事件:clickmousedownmouseupkeydownkeyupkeypress

  • Js中事件是否支持冒泡

对于focus,blur,change,submit,reset,select等不会冒泡的事件,在标准游览器中,我们可以设置addEventListener的最后一个参数为true轻松搞定。
IE就有点麻烦,要用focusin代替focus,focusout代替blur,selectstart代替select。
change,submit与reset就复杂了,必须利用其他事件来模拟,还要判断事件源的类型,selectedIndex,keyCode等等,jQuery有插件用很复杂的方式来实现……
onselect事件发生在mouseup事件之后,而onselectstart 事件发生在mousedown并mousemove事件之后。

如果元素被阻止冒泡了,千万别去用事件委托的方式监听事件,因为事件委托的原理是利用事件冒泡,当冒泡被阻止,就无法监听了。

冒泡,捕获,委托三者关系

事件捕获和冒泡是现代浏览器的执行事件的两个不同阶段

事件委托是利用冒泡阶段的运行机制来实现的

所有冒泡皆可取消,默认动作有的可以取消有的不能取消。

  • 滚动事件scroll event

image.png

如果真想要阻止滚动,可以阻止wheel和touchstart的默认动作。
注意:你还有找准滚动条所在的元素,但是滚动条还能用,可以css让滚动条display: none;或者overflow: hidden可以直接取消滚动条,但此时JS仍然可以修改scrollTop。

Cancelable是用来取消(也可以说是阻止)默认动作的。

事件绑定的两种方法

  • DOM0级事件绑定 elmemnt.onclick=function(){}

  • DOM2级事件绑定

  • 标准浏览器:curEle.addEventListener('click',function(){},false)
  • IE6-8:curEle.attachEvent('onclick',function(){})

DOM2事件流三个阶段:捕获阶段,目标阶段,冒泡阶段

如何阻止冒泡事件

  • 在支持addEventListener()的浏览器中,可以调用事件对象的stopPropagation()方法以阻止事件的继续传播。如果在同一对象上定义了其他处理程序,剩下的处理程序将依旧被调用,但调用stopPropagation()之后任何其他对象上的事件处理程序将不会被调用。不仅可以阻止事件在冒泡阶段的传播,还能阻止事件在捕获阶段的传播。

event.stopPropagation()

  • IE9之前的IE不支持stopPropagation()方法,而是设置事件对象cancelBubble属性为true来实现阻止事件进一步传播。

e.cancelBubble = true

取消默认事件

w3c的方法是e.preventDefault(),IE则是使用e.returnValue = false;

javascript的return false只会阻止默认行为,而是用[jQuery]的话则既阻止默认行为又防止对象冒泡。

JS支持事件吗

不支持,DOM事件不属于JS的功能,属于浏览器提供的DOM的功能。

JS只是调用了DOM提供的addEventListener而已。

事件捕获和冒泡的区别

执行顺序不同,触发顺序是:先捕获,后冒泡

// css
    #parent {
      width: 300px;
      height: 150px;
      padding-top: 25px;
      text-align: center;
      color:#fff;
      background-color: blue;
    }
    #child {
      width: 200px;
      height: 100px;
      margin: 0 auto;
      line-height: 100px;
      background-color: red;
    }
// js  
    parent.addEventListener('click', () => {
      console.log('冒泡parent')
    })
    child.addEventListener('click', () => {
      console.log('冒泡child')
    })
    parent.addEventListener('click', () => {
      console.log('捕获parent')
    },true)
    child.addEventListener('click', () => {
      console.log('捕获child')
    }, true)

image.png

  • 点击父级元素,先执行捕获,再执行冒泡

image.png

  • 点击子元素,先捕获父级-子级,再执行冒泡子级-父级

image.png

  • 点击空白区域时,跟点击父级元素一样,这怎么理解呢?

哈哈。。。这里的parent指向了window,没有按标准写法来。