事件捕获、事件冒泡、事件委托

265 阅读7分钟

一、示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="myDiv">
        <ul id="myUl">
            <li id="myLi">1</li>
        </ul>
    </div>
</body>
<script>
    let oDiv = document.getElementById('myDiv');
    let oUl = document.getElementById('myUl');
    let oLi = document.getElementById('myLi');
    window.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName);
    },true);
    document.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName);
    },true);
    document.body.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName);
    },true);
    oDiv.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget);
    },true);
    oUl.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget);
    },true);
    oLi.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget);
    },true)
    window.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName);
    },false);
    document.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName);
    },false);
    document.body.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName);
    },false);
    oDiv.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget);
    },false);
    oUl.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget);
    },false);
    oLi.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget);
    },false)
</script>
</html>


DOM2的事件流:事件捕获、处于目标状态、事件冒泡。

目标源(target)为:li,当鼠标点击后,会发生事件捕获。

捕获阶段:首先window会捕获事件,之后document、body会捕获到事件,再往后div、ul一层一层的捕获到事件。

目标阶段:li事件发生了两次,因为在捕获阶段为其绑定了事件,在冒泡阶段也绑定了事件,所以发生了两次。note:处于目标阶段的事件不按照事件捕获-事件冒泡的顺序执行,而是按其绑定事件的顺序执行。

冒泡阶段:与捕获阶段相反的步骤,将事件一步一步冒泡到window。

解释:target是真正发生事件的dom元素。currentTarget是当前事件发生在那个dom上。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="myDiv">
        <ul id="myUl">
            <li id="myLi">1</li>
        </ul>
    </div>
</body>
<script>
    let oDiv = document.getElementById('myDiv');
    let oUl = document.getElementById('myUl');
    let oLi = document.getElementById('myLi');
    window.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName);
    },true);
    document.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName);
    },true);
    document.body.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName);
    },true);
    oDiv.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget);
    },true);
    oUl.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget);
    },true);
    //事件源的冒泡事件在捕获之前
    oLi.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget);
    },false)
    oLi.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget);
    },true)
    window.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName);
    },false);
    document.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName);
    },false);
    document.body.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName);
    },false);
    oDiv.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget);
    },false);
    oUl.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget);
    },false);
</script>
</html>


处于目标阶段的dom事件是按其绑定事件的顺序执行。

问题1:测试onclick事件发生在那个阶段?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="myDiv">
        <ul id="myUl">
            <li id="myLi">1</li>
        </ul>
    </div>
</body>
<script>
    let oDiv = document.getElementById('myDiv');
    let oUl = document.getElementById('myUl');
    let oLi = document.getElementById('myLi');
    oDiv.onclick = function(){
        console.log("测试onclick事件发生在那个阶段?" + this);
    };
    window.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName + this);
    },true);
    document.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName  + this);
    },true);
    document.body.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName  + this);
    },true);
    oDiv.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget.nodeName  + this);
    },true);
    oUl.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget.nodeName  + this);
    },true);
    oLi.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget.nodeName  + this);
    },true)
    window.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName  + this);
    },false);
    document.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName + this);
    },false);
    document.body.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName + this);
    },false);
    // oDiv.addEventListener('click',function(ev){
    //     var ev = ev || window.event;
    //     let target = ev.target || ev.srcElement;
    //     console.log('冒泡' + ev.target + ev.currentTarget);
    // },false);
    oUl.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget.nodeName + this);
    },false);
    oLi.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget.nodeName  + this);
    },false)
</script>
</html>


onclick直接绑定的事件发生在冒泡阶段。

事件绑定、解绑、阻止默认

事件绑定:

1、直接获取元素绑定

element.onclick = function(e){
        // ...
    };

优点:简单稳定,浏览器兼容性好。

缺点:

(1)只会在事件冒泡中执行;this指向当前元素。

(2)一个元素只能绑定一个事件处理函数,新绑定的事件处理函数会覆盖旧的事件处理函数;

2、w3c方法:

element.addEventListener('click', function(e){
        // ...
    }, false);

优点:

(1)该方法同时支持事件冒泡和事件捕获。事件阶段取决于事件的第三个参数:true(捕获)、false(冒泡)。this指向当前元素。

(2)可以为同一个元素绑定多个事件,而且不会覆盖先绑定的事件。

缺点:IE不支持,必须使用IE的attachEvent函数代替。

element.attachEvent('onclick', function(){
        // ...
});

note:

(1)IE中this指向window。

(2)事件名必须以ontype形式命名,不能用type形式。

(3)IE只支持事件的冒泡阶段。

解除事件:

element.removeEventListener('click', function(e){
        // ...
    }, false);

IE:

element.detachEvent('onclick', function(){
        // ...
});

阻止事件传播

stopPropagation()方法不仅可以阻止事件在冒泡阶段的传播,还能阻止事件在捕获阶段的传播。stopPropagation()很少用到在捕获阶段去阻止事件的传播,大家就以stopPropagation()只能阻止事件在冒泡阶段传播。

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="myDiv">
        <ul id="myUl">
            <li id="myLi">1</li>
        </ul>
    </div>
</body>
<script>
    let oDiv = document.getElementById('myDiv');
    let oUl = document.getElementById('myUl');
    let oLi = document.getElementById('myLi');
    oDiv.onclick = function(){
        console.log("测试onclick事件发生在那个阶段?" + this);
    };
    window.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName + this);
    },true);
    document.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName  + this);
    },true);
    document.body.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target.nodeName + ev.currentTarget.nodeName  + this);
    },true);
    oDiv.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget.nodeName  + this);
        //在捕获阶段阻止事件传播
        ev.stopPropagation();
    },true);
    oUl.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget.nodeName  + this);
    },true);
    oLi.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('捕获' + ev.target + ev.currentTarget.nodeName  + this);
    },true)
    window.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName  + this);
    },false);
    document.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName + this);
    },false);
    document.body.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target.nodeName + ev.currentTarget.nodeName + this);
    },false);
    oUl.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget.nodeName + this);
    },false);
    oLi.addEventListener('click',function(ev){
        var ev = ev || window.event;
        let target = ev.target || ev.srcElement;
        console.log('冒泡' + ev.target + ev.currentTarget.nodeName  + this);
    },false)
</script>
</html>


阻止事件的默认行为:

preventDefault()可以阻止事件的默认行为发生,默认行为是指:点击a标签就转跳到其他页面、拖拽一个图片到浏览器会自动打开、点击表单的提交按钮会提交表单等等,因为有的时候我们并不希望发生这些事情,所以需要阻止默认行为。

IE9之前的IE中,可以通过设置事件对象的returnValue属性为false达到同样的效果。

function cancelHandler(event){
    var event=event||window.event;//兼容IE
    
    //取消事件相关的默认行为
    if(event.preventDefault)    //标准技术
        event.preventDefault();
    if(event.returnValue)    //兼容IE9之前的IE
        event.returnValue=false;
    return false;    //用于处理使用对象属性注册的处理程序
}

事件委托

1、为什么要使用事件委托?

在javascript中,添加到页面上的处理程序的个数直接影响到页面运行的性能。导致这一问题的原因是多方面的:

(1)每个函数都是一个对象,都会占用内存。内存中的对象越多,性能就越差。

(2)必须事先指定所有的事件处理程序而导致的dom返问次数,会延迟整个页面的交互。

2、事件委托的原理

事件委托的原理是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。触发目标试剑石,它其实也要经过事件捕获、处于目标阶段、事件冒泡三个阶段。对于事件委托来说,在事件捕获或者事件冒泡阶段处理并没有优劣之分,从兼容性的角度来说,建议使用事件冒泡模型。

示例可以看上一篇tab标签。