超全面的js事件委托机制


这两个是我看到的比较全面讲解事件委托机制的文章:

知乎文章

掘金文章

北风吹雪的文章

掘金

作者:wang二狗
链接:https://juejin.im/post/5e739534e51d4526f23a4150
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

要理解DOM相关事件,我们先要理解“事件流”这个概念,事件流描述的是从页面中接收事件的顺序。

事件冒泡:事件开始由最具体的元素接收,然后逐级向上传播到较为不具体的节点或文档。

事件捕获:事件开始由不太具体的节点接收,然后逐级向下传播到最具体的节点。它与事件冒泡是个相反的过程。

DOM2 级事件规定的事件流包括三个阶段:事件捕获、目标阶段、事件冒泡。

事件委托

事件委托,通俗的说就是将元素的事件委托给它的父级或者更外级的元素处理,它的实现机制就是事件冒泡。

假设有一个列表,要求点击列表项弹出对应的字段:

<ul id="myLink">
  <li id="1">aaa</li>
  <li id="2">bbb</li>
  <li id="3">ccc</li>
</ul>

不使用事件委托

var myLink = document.getElementById('myLink');
var li = myLink.getElementsByTagName('li');

for(var i = 0; i < li.length; i++) {
  li[i].onclick = function(e) {
    var e = event || window.event;
    var target = e.target || e.srcElement;
    alert(e.target.id + ':' + e.target.innerText);  
  };
}

存在问题:

  • 给每一个列表都绑定事件,消耗内存
  • 当有动态添加的元素时,需要重新给元素绑定事件

8k的前端写出的事件委托

事实上很多同学在网上看到的事件委托的方法都是错的,虽然是错的,但是你面试的时候也可能会过,因为面你的面试官可能也不知道正确的事件委托应该怎么写。

下面我们就来看一下错误版的事件委托是怎么写的:

 ul.addEventListener('click', function(e){
     if(e.target.tagName.toLowerCase() === 'li'){
         fn() // 执行某个函数
     }
 })

20k的前端写出的事件委托

错误版事件委托的bug 在于,如果用户点击的是 li里面的 span,就没法触发 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)
     })
     return element
   }

思路是点击 span后,递归遍历 span 的祖先元素看其中有没有 ul 里面的 li

事件委托的优点

  • 只需要将同类元素的事件委托给父级或者更外级的元素,不需要给所有的元素都绑定事件,减少内存占用空间,提升性能。
  • 动态新增的元素无需重新绑定事件

需要注意的点

  • 事件委托的实现依靠的冒泡,因此不支持事件冒泡的事件就不适合使用事件委托。
  • 不是所有的事件绑定都适合使用事件委托,不恰当使用反而可能导致不需要绑定事件的元素也被绑定上了事件。

告诫自己,即使再累也不要忘记学习,成功没有捷径可走,只有一步接着一步走下去。 共勉!

知乎

什么是事件机制

JavaScript 事件机制描述的是事件在 DOM 里面的传递顺序,以及我们可以对这些事件做出如何的响应。

DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。

事件捕获(event capturing): 通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。

事件冒泡(dubbed bubbling): 与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。无论是事件捕获还是事件冒泡,它们都有一个共同的行为,就是事件传播,它就像一跟引线,只有通过引线才能将绑在引线上的鞭炮(事件监听器)引爆, 试想一下,如果引线不导火了,那鞭炮就只有一响了!!!

dom标准事件流的触发的先后顺序为 :先捕获再冒泡,即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡

不同的浏览器对此有着不同的实现,IE10及以下不支持捕获型事件,所以就少了一个事件捕获阶段,IE11、Chrome 、Firefox、Safari等浏览器则同时存在。

说到事件冒泡与捕获就不得不提一下两个用于事件绑定的方法addEventListener 、

attachEvent 。当然还有其它的事件绑定的方式这里不做介绍。

addEventListener(event, listener, useCapture)

·参数定义:event—(事件名称,如click,不带on), listener—事件监听函数,

useCapture—是否采用事件捕获进行事件捕捉, 默认为false,即采用事件冒泡方式

addEventListener在 IE11、Chrome 、Firefox、Safari等浏览器都得到支持。

attachEvent(event,listener)

·参数定义:event—(事件名称,如onclick,带on), listener—事件监听函数。

attachEvent主要用于IE浏览器,并且仅在IE10及以下才支持,IE11已经废了这个方法了

(微软还是挺识趣的,慢慢向标准靠拢)。

事件冒泡

事件开始时由最具体的元素接受,然后逐级向上传播到较为不具体的元素

<html lang="zh-cn">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>js事件机制</title>
        <style>
            #parent{
                width: 200px; height:200px;
                text-align: center; line-height: 3;
                background: green;
            }
            #child{
                width: 100px; height: 100px; margin: 0 auto; background: orange;
            }
</style>
    </head>
    <body>
        <div id="parent"> 父元素
            <div id="child"> 子元素
            </div>
        </div>
        <script type="text/javascript">
            var parent = document.getElementById("parent");
            var child = document.getElementById("child");

            document.body.addEventListener("click", function(e) {
                console.log("click-body");
            }, false);

            parent.addEventListener("click", function(e) {
                console.log("click-parent");
            }, false);

            child.addEventListener("click", function(e) {
                console.log("click-child");
            }, false);
        </script>
    </body>
</html>

通过”addEventListener”方法,采用事件冒泡方式给dom元素注册click事件,点击子元素会发生什么呢?如果你对事件冒泡有一定了解的话那你肯定知道上面的代码会输出的顺序,没错,如下图所示:

事件触发顺序是由内到外的,这就是事件冒泡,虽然只点击子元素,但是它的父元素也会触发相应的事件,其实这是合理的,因为子元素在父元素里面,点击子元素也就相当于变相的点击了父元素,这样理解对吧?这里有同学可能要问了,如果点击子元素不想触发父元素的事件怎么办?肯定可以的,那就是停止事件传播—event.stopPropagation();

事件捕获

不太具体的节点更早接受事件,而最具体的元素最后接受事件,和事件冒泡相反修改上面栗子中的代码,给parent元素注册一个捕获事件,如下

var parent = document.getElementById("parent");
var child = document.getElementById("child");

document.body.addEventListener("click", function(e) {
    console.log("click-body");
}, false);

parent.addEventListener("click", function(e) {
    console.log("click-parent---事件传播");
}, false);
//新增事件捕获事件代码
parent.addEventListener("click", function(e) {
    console.log("click-parent--事件捕获");
}, true);
child.addEventListener("click", function(e) {
    console.log("click-child");
}, false);

如果你看明白了我前面说的那些,你就知道这个栗子的输出顺序了。

父元素通过事件捕获的方式注册了click事件,所以在事件捕获阶段就会触发,然后到了目标阶段,即事件源,之后进行事件传播,parent同时也用冒泡方式注册了click事件,所以这里会触发冒泡事件,最后到根节点。这就是整个事件流程。

DOM事件流

DOM2级事件规定事件流包括三个阶段,事件捕获阶段,处于目标阶段,时间冒泡阶段, 首先发生的是事件捕获,为截取事件提供机会,然后是实际目标接受事件,最后是冒泡阶段

注:Opera、Firefox、Sarfari都支持DOM事件流,IE不支持事件流,只支持时间冒泡

当一个事件发生以后,它会在不同的DOM节点之间传播(propagation)。这种传播分为三个阶段:

第一阶段:从window对象传导到目标节点,称为“捕获阶段”(capture phase)。第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。

第三阶段:从目标节点传导回window对象,称为“冒泡阶段”(bubbling phase)。

事件委托

事件委托其实也叫事件代理。

定义:事件代理就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。(delegation)。

var ul = document.querySelector('ul'); ul.addEventListener('click', function(event){
if(event.target.tagName.toLowerCase() === 'li'){
//...
}})

上面代码的click 事件的监听函数定义在<ul> 节点,但是实际上,它处理额是子节点<li> 的click事件。这样的好处是,只要定义一个监听函数,就能处理多个子节点的事件,且以后再添加子节点,监听函数依然有效。

那什么样的事件可以用事件委托,什么样的事件不可以用呢?

适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress 。值得注意的是,mouseover 和mouseout 虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。

不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在不如说focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。

更加详细内容可查看:Js 中事件绑定、事件代理和事件委托

误区

在同一个对象上注册事件,并不一定按照注册顺序执行

之所以如此是因为事件目的地节点既绑定了冒泡事件也绑定了捕获事件,此时的执行顺序按照绑定的先后顺序执行(情况比较少见)。

举例

<html lang="zh-cn">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>js事件机制</title>
        <style>
            #parent{
                width: 200px; height:200px;
                text-align: center; line-height: 3; background: green;
            }
            #child{
                width: 100px; height: 100px; margin: 0 auto; background: orange;
            }
</style>
    </head>
    <body>
        <div id="parent"> 父元素
            <div id="child"> 子元素
            </div>
        </div>
        <script type="text/javascript">
            var parent = document.getElementById("parent");
            var child = document.getElementById("child");

            // document.body.addEventListener("click",function(e){
            //    console.log("click-body");
            // },false); child.addEventListener("click",function(e){ console.log("click-child");
            }, false);

            child.addEventListener("click", function(e) {
                console.log("click-child-捕获");
            }, true);
            parent.addEventListener("click", function(e) {
                console.log("click-parent");
            }, false);



            parent.addEventListener("click", function(e) {
                console.log("click-parent-捕获");
            }, true);
        </script>
    </body>
</html>

点击子DIV执行结果:

交换子div事件,如下:

<html lang="zh-cn">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>js事件机制</title>
        <style>
            #parent{
                width: 200px; height:200px;
                text-align: center; line-height: 3; background: green;
            }
            #child{
                width: 100px; height: 100px; margin: 0 auto; background: orange;
            }
</style>
    </head>
    <body>
        <div id="parent"> 父元素
            <div id="child"> 子元素
            </div>
        </div>
        <script type="text/javascript">
            var parent = document.getElementById("parent");
            var child = document.getElementById("child");

            // document.body.addEventListener("click",function(e){
            //    console.log("click-body");
            // },false);

            child.addEventListener("click", function(e) {
                console.log("click-child-捕获");


            }, true);
            child.addEventListener("click", function(e) {
                console.log("click-child");
            }, false);


            parent.addEventListener("click", function(e) {
                console.log("click-parent");
            }, false);


            parent.addEventListener("click", function(e) {
                console.log("click-parent-捕获");
            }, true);
        </script>
    </body>
</html>

点击子DIV执行结果:

由于子DIV上绑定了捕获和冒泡事件,所以此时的执行顺序是按照绑定的执行顺序。实际这种情况很少。

event.stopPropagation()

就是阻止事件的冒泡

这个表述不能说他错误,但是是不完整的,他除了阻止事件的冒泡,还阻止事件的继续捕获,简而言之就是阻止事件的进一步传播 。下面的例子可以看到:

<html lang="zh-cn">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>js事件机制</title>
        <style>

            #parent{
                width: 200px; height:200px;
                text-align: center; line-height: 3; background: green;
            }
            #child{
                width: 100px; height: 100px; margin: 0 auto; background: orange;
            }
</style>
    </head>
    <body>
        <div id="parent"> 父元素
            <div id="child"> 子元素
            </div>
        </div>
        <script type="text/javascript">
            var parent = document.getElementById("parent");
            var child = document.getElementById("child");


            child.addEventListener("click", function(e) {
                console.log("click-child-捕获");
            }, true);
            //    child.addEventListener("click",function(e){
            //    console.log("click-child");
            // },false);

            // parent.addEventListener("click",function(e){
            //    console.log("click-parent");
            // },false);

            parent.addEventListener("click", function(e) {
                event.stopPropagation();
                console.log("click-parent-捕获");
            }, true);
        </script>
    </body>
</html>

执行结果:

eturn false;阻止默认行为

return false;事件处理过程中,阻止了事件冒泡,也阻止了默认行为(比如刚才它就没有执行超链接的跳转)

return false 不仅阻止了事件往上冒泡,而且阻止了事件本身。event.stopPropagation() 则只阻止事件的进一步传播,不阻止事件本身。

拓展

stopImmediatePropagation 的使用

这玩意儿是 w3c 的东西,使用的也不是特别多,我们知道 stopPropagation 可以阻止事件的进一步传播,但是他阻止不了该元素上绑定的其他函数的执行,比如我们在 obj 上绑定了 func1 和 func2,如果我们在 func1 中使用了 stopPropagation ,那 func2 依然还是会执行出来。倘若这里使用 stopImmediatePropagation,结果就不一样了,他不仅阻止事件的传播,还阻止 func2 的执行。如:

结果是:

而改成evt.stopImmediatePropagation();之后,阻止了第二个监听事件的触发:

结果是:

setCapture 和 releaseCapture

这两个是 IE 下的事件绑定函数,只要我们在某个元素上 setCapture 了,那么你在任何地方的鼠标操作(mouseXXX之类的动作)都会在这个元素上触发(前提是你在这个元素上绑定了事件),releaseCapture 或者本窗口失去聚焦才会释放这个绑定~

其余文章

DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。

事件捕获*(\***event capturing***\)*:通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内**进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。

事件冒泡\*(***dubbed bubbling***)**:*与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。

无论是事件捕获还是事件冒泡,它们都有一个共同的行为,就是事件传播,它就像一跟引线,只有通过引线才能将绑在引线上的鞭炮(事件监听器)引爆,试想一下,如果引线不导火了,那鞭炮就只有一响了!!!

 dom标准事件流的触发的先后顺序为:先捕获再冒泡,即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。不同的浏览器对此有着不同的实现,IE10及以下不支持捕获型事件,所以就少了一个事件捕获阶段,IE11、Chrome 、Firefox、Safari等浏览器则同时存在。

说到事件冒泡与捕获就不得不提一下两个用于事件绑定的方法addEventListener、attachEvent。当然还有其它的事件绑定的方式这里不做介绍。 

  addEventListener(event, listener, useCapture)\  

    ·参数定义:event—(事件名称,如click,不带on),listener—事件监听函数,useCapture—是否采用事件捕获进行事件捕捉,

        默认为false,即采用事件冒泡方式

    addEventListener在 IE11、Chrome 、Firefox、Safari等浏览器都得到支持。

  attachEvent(event,listener)\  

    ·参数定义:event—(事件名称,如onclick,带on),listener—事件监听函数。

    attachEvent主要用于IE浏览器,并且仅在IE10及以下才支持,IE11已经废了这个方法了(微软还是挺识趣的,慢慢向标准靠拢)。

说了一箩筐定义,下面就用上面这两个方法通过栗子来解释一下事件捕获与事件冒泡的具体表现行为差异。

事件冒泡

栗1:

<html lang="zh-cn">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>js事件机制</title>
    <style>
        #parent{
            width: 200px;
            height:200px;
            text-align: center;
            line-height: 3;
            background: green;
        }
        #child{
            width: 100px;
            height: 100px;
            margin: 0 auto;
            background: orange;
        }
    </style>
    </head>
<body>
    <div id="parent">
        父元素
        <div id="child">
            子元素
        </div>
    </div>
    <script type="text/javascript">
        var parent = document.getElementById("parent");
        var child = document.getElementById("child");

        document.body.addEventListener("click",function(e){
            console.log("click-body");
        },false);

        parent.addEventListener("click",function(e){
            console.log("click-parent");
        },false);

        child.addEventListener("click",function(e){
            console.log("click-child");
        },false);
    </script>
</body>
</html>

通过”addEventListener”方法,采用事件冒泡方式给dom元素注册click事件,点击子元素会发生什么呢?如果你对事件冒泡有一定了解的话那你肯定知道上面的代码会输出的顺序,没错,如下图所示:

事件触发顺序是由内到外的,这就是事件冒泡,虽然只点击子元素,但是它的父元素也会触发相应的事件,其实这是合理的,因为子元素在父元素里面,点击子元素也就相当于变相的点击了父元素,这样理解对吧?

这里有同学可能要问了,如果点击子元素不想触发父元素的事件怎么办?肯定可以的,那就是停止事件传播—event.stopPropagation();

修改栗1的代码,在子元素的监听函数中加入停止事件传播的操作

栗2:

child.addEventListener("click",function(e){
  console.log("click-child");
   e.stopPropagation();
},false);

在点击子元素的时候就只弹出了子元素那条信息,父元素的事件没有触发,因为事件已经停止传播了,冒泡阶段也就停止了。

事件冒泡差不多就讲述完了,别急,捕获还没说呢!

事件捕获

栗3,修改栗子1中的代码,给parent元素注册一个捕获事件,如下

var parent = document.getElementById("parent");
        var child = document.getElementById("child");

        document.body.addEventListener("click",function(e){
            console.log("click-body");
        },false);

        parent.addEventListener("click",function(e){
            console.log("click-parent---事件传播");
        },false);
          //新增事件捕获事件代码
        parent.addEventListener("click",function(e){
            console.log("click-parent--事件捕获");
        },true);

        child.addEventListener("click",function(e){
            console.log("click-child");
        },false);

如果你看明白了我前面说的那些,你就知道这个栗子的输出顺序了。

父元素通过事件捕获的方式注册了click事件,所以在事件捕获阶段就会触发,然后到了目标阶段,即事件源,之后进行事件传播,parent同时也用冒泡方式注册了click事件,所以这里会触发冒泡事件,最后到根节点。这就是整个事件流程。

上面介绍了事件冒泡、事件捕获、事件传播,下面讲一下如果通过以上三个知识点进行事件**委托**

委托在JQuery中已经得到了实现,即通过$(selector).on(event,childSelector,data,function,map)实现委托,一般用于动态生成的元素,当然JQuery也是通过原声的js去实现的,下面举一个简单的栗子,通过js实现通过parent元素给child元素注册click事件

var parent = document.getElementById("parent");
var child = document.getElementById("child");
parent.onclick = function(e){
            if(e.target.id == "child"){
                console.log("您点击了child元素")
            }
}

虽然没有直接只child元素注册click事件,可是点击child元素时却弹出了提示信息。


评论
  目录