事件流

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

事件分为三个阶段:

事件捕获阶段

目标事件阶段

事件冒泡阶段

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

事件捕获是指的不太具体的节点应该先接受到事件,而最具体的节点应该最后接受到事件。事件捕获的意义在于事件到达预定目标之前捕获它

事件触发 Java实现_事件处理

实际上,目标在捕获阶段不会接受到事件

在使用案例说明之前,先说明几个对象:

event对象中有三个属性,在之后会被使用到:

1. eventPhase属性返回事件传播的当前阶段

常量


Event.CAPTURING_PHASE

1

Event.AT_TARGET

2

Event.BUBBLING_PHASE

3

2. Target表示触发事件的节点

3. 事件当前节点

4. addEventListener的第三个参数是个布尔值,如果是true表示在事件捕获阶段调用事件处理程序;如果是false表示在事件冒泡阶段调用事件处理程序

第一种情况,不使用addEventListener事件,之间让元素捆绑onclick事件:

<body>
<div id="wrapDiv">wrapDiv
    <p id="innerParent">innerParent
        <span id="textSpan">textSpan</span>
    </p>
</div>
</body>



var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerParent");
    var textSpan = document.getElementById("textSpan");
    wrapDiv.onclick = function(e){
        console.log( "wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }
    innerP.onclick = function(e){
        console.log( "innerP", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }
    textSpan.onclick = function(e){
        console.log( "textSpan", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }
    document.body.onclick = function(e){
        console.log( "body", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }
    document.onclick = function(e){
        console.log( "document", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }
    window.onclick = function(e){
        console.log( "window", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }

事件触发 Java实现_事件触发 Java实现_02

点击textSpan后控制台的输出:

事件触发 Java实现_事件冒泡_03

说明了只有被点击元素是在目标阶段触发,而其他元素都是在冒泡阶段被触发。

第二种情况,使用addEventListener,并且将第三个参数置为false,发现结果和第一种情况是一样的:

window.addEventListener('click',function (e) {
        console.log("window", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    },false)
    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);
    document.body.addEventListener("click", function(e){
        console.log("body", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

第三种情况,在冒泡和捕获阶段都绑定监听

//冒泡阶段
    window.addEventListener('click',function (e) {
        console.log("window", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    },false)
    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);
    document.body.addEventListener("click", function(e){
        console.log("body", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);
    //捕获阶段
    window.addEventListener('click',function (e) {
        console.log("window", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    },true)
    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);
    document.body.addEventListener("click", function(e){
        console.log("body", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

事件触发 Java实现_事件处理_04

根据控制台的输出可以看出,点击之后捕获阶段先发生,window最先捕获到->document->body->wrapdiv->innerP

textSpan目标阶段事件发生了两次,因为textSpan即在捕获阶段绑定了事件,又在冒泡阶段绑定了事件。

冒泡事件的过程与捕获相反


第四种情况,对目标事件在冒泡、捕获阶段都进行事件监听

textSpan.addEventListener("click", function(e){
        console.log("textSpan addEventListener true",e.eventPhase)
    },true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan addEventListener false",e.eventPhase)
    }, false);

    textSpan.onclick = function(e){
        console.log("textSpan onclick",e.eventPhase)
    };

事件触发 Java实现_JS_05

textSpan.addEventListener("click", function(e){
        console.log("textSpan addEventListener false",e.eventPhase)
    }, false);
    textSpan.onclick = function(e){
        console.log("textSpan onclick",e.eventPhase)
    };
    textSpan.addEventListener("click", function(e){
        console.log("textSpan addEventListener true",e.eventPhase)
    },true);

事件触发 Java实现_事件捕获_06

根据两次运行的结果可以发现,textSpan作为被点击的目标元素,所有绑定在textSpan上的事件都会被触发,触发的顺序只和事件绑定顺序有关系

第四种情况,如果给父元素也添加onclick的事件监听,那么发生在哪个阶段呢?

textSpan.addEventListener("click", function(e){
        console.log("textSpan addEventListener false",e.eventPhase)
    }, false);
    wrapDiv.onclick = function(e){
        console.log( "wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }
    textSpan.onclick = function(e){
            console.log("textSpan onclick",e.eventPhase)
        };
    wrapDiv.onclick = function(e){
        console.log( "wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }
    textSpan.addEventListener("click", function(e){
        console.log("textSpan addEventListener true",e.eventPhase)
    },true);
    wrapDiv.onclick = function(e){
        console.log( "wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }

事件触发 Java实现_事件处理_07

wrap不是目标元素,所以它绑定的事件会遵循先发生捕获后冒泡的原则,上例子很明显,onclick事件发上在了冒泡阶段。而且虽然写了三次,但是只会被在冒泡阶段执行一次。如果点击wrap元素,也只会在目标阶段执行一次。

关于阻止冒泡事件

event.stopPropagation()

//冒泡阶段
    window.addEventListener('click',function (e) {
        console.log("window", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    },false)
    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);
    document.body.addEventListener("click", function(e){
        console.log("body", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    textSpan.addEventListener("click", function(e){
        e.stopPropagation()
        console.log("textSpan", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);
    //捕获阶段
    window.addEventListener('click',function (e) {
        console.log("window", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    },true)
    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);
    document.body.addEventListener("click", function(e){
        console.log("body", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);



点击目标元素textSpan后输入如下

事件触发 Java实现_事件处理_08

跟第一种情况比较,可以发现事件捕获阶段到目标阶段后就结束了,冒泡阶段被阻止了。


stopPropagation()不仅可以在阻止事件的冒泡传播,还可以阻止事件的捕获传播

//冒泡阶段
    window.addEventListener('click',function (e) {
        console.log("window", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    },false)
    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);
    document.body.addEventListener("click", function(e){
        console.log("body", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);
    //捕获阶段
    window.addEventListener('click',function (e) {
        console.log("window", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    },true)
    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);
    document.body.addEventListener("click", function(e){
        console.log("body", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        e.stopPropagation()
        console.log("wrapDiv", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan", e.target.nodeName, e.currentTarget.nodeName,e.eventPhase);
    }, false);

点击textSpan事件后输出如下

事件触发 Java实现_JS_09


最后附赠最近被面试问到的一个题目:

页面有元素A和B,B被包裹在A中,需要实现以下几个场景:

先触发A,在触发B

只触发A

只触发B

先触发B,在触发A

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #A{
            width: 100px;
            height: 100px;
            background-color: pink;
            position: relative;
        }
        #B{
            width: 50px;
            height: 50px;
            background-color: deepskyblue;
            position: absolute;
            top:50%;
            left:50%;
            transform: translate(-50%,-50%);
        }
    </style>
</head>
<body>
<div id="A">
    A
    <div id="B">B</div>
</div>
</body>
<script>

    var A = document.getElementById("A");
    var B = document.getElementById("B");

    //先触发A,在触发B
    A.addEventListener('click',function () {
        alert("A")
    },true)//表示在捕获阶段执行
    B.addEventListener('click',function () {
        alert("B")
    })

    //只触发A
    A.addEventListener('click',function (event) {
        alert("A")
        event.stopPropagation()//阻止事件向下传递
    },true)//表示在捕获阶段执行
    B.addEventListener('click',function () {
        alert("B")
    })
    
    //只触发B
    A.addEventListener('click',function (event) {
        alert("A")
    })
    B.addEventListener('click',function () {
        alert("B")
        event.stopPropagation()//阻止事件冒泡
    })

    //先触发B,在触发A
    A.addEventListener('click',function () {
        alert("A")
    })//事件在冒泡阶段执行
    B.addEventListener('click',function () {
        alert("B")
    })
</script>
</html>