深入理解js Dom事件機制(一)——事件流

theshy發表於2017-11-10

首先我們思考一個很有意思的事情:一張紙上畫了兩個同心圓,當我們把手指放到圓心上時,手指指向的不是一個圓,而是紙上的兩個圓,同理之,當我們單擊網頁上的一個div塊的時候(程式碼片段一),單擊事件會僅僅作用在這個div上面嗎? 在瀏覽器發展到第四代時,IE和Netscape的開發團隊都遇到這個問題,他們都一致認為,除了單擊div塊,我們也單擊了body、 html、甚至是整個document,但不幸的是兩個團隊針對事件流模型產生了兩個完全相反的概念。

程式碼片段一:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>事件流</title>
</head>
<body>
    <div id="box">Click me</div>
</body>
</html>

1. 事件冒泡(推薦)

IE的事件流稱為事件冒泡。
即:事件由最具體的元素接收(div),逐級向上傳播到不具體的節點(document)。

當我們點選程式碼片段一中id為box的div塊時,單擊事件會按照如下順序傳播:
div ——> body——> html ——> document

圖片描述

如上圖所示,click首先在div元素上發生,然後沿著Dom樹向上傳播,每一級節點都會發生直至傳播到document物件。

測試程式碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>事件流</title>
</head>
<style type="text/css">
    #box1{
        width: 300px;
        height: 300px;
        background-color: red;
    }
    #box2{
        width: 200px;
        height: 200px;
        background-color: yellow;
    }
    #box3{
        height: 100px;
        width: 100px;
        background-color: green;
    }
</style>
<body>
    <div id="box1">
        <div id="box2">
            <div id="box3"></div>
        </div>
    </div>
</body>
<script type="text/javascript">
    document.getElementById('box1').onclick = function () {
        console.log('box1 click')
    }
    document.getElementById('box2').onclick = function () {
        console.log('box2 click')
    }

    document.getElementById('box3').onclick = function () {
        console.log('box3 click')
    }
</script>
</html>

測試效果:

圖片描述

note: 幾乎現代所有的瀏覽器都支援事件冒泡,不過有一些細微的差別
IE5.5 和 IE5.5 - 版本的事件冒泡會跳過html元素(body 直接到 document)
IE9、Firefox、Chrome、Safari則一直冒泡到window物件。

2、事件捕獲

Netscape提出的事件流模型稱為事件捕獲。
即:事件從最不具體的節點開始接收(document),傳遞至最具體的節點<div>,和IE的冒泡剛好相反, 事件捕獲的本意是當事件到達預定目標前捕獲它。

當我們點選程式碼片段一中id為box的div塊時,單擊事件會按照如下順序傳播:
document——> html ——> body ——> div

010945579257474.png

note: 雖然事件捕獲是Netscape唯一支援的事件流模型,但IE9、Firefox、Chrome、Safari目前也都支援這種事件模型,由於老版本的瀏覽器並不支援,所以我們應該儘量使用事件冒泡,有特殊需求的時候再考慮事件捕獲。

3、DOM2級事件流

為了能夠相容上述兩種事件模型,又提出了一個DOM2級事件模型,它規定了事件流包含三個階段:

  • 事件捕獲階段:為事件捕獲提供機會;

  • 處於目標階段:事件的目標接收到事件(但並不會做出響應);

  • 事件冒泡階段:事件響應階段;

在DOM2級事件流中,但我們點選程式碼片段一中的div,在事件捕獲階段從document ->html ->body就停止了(div元素在這個階段並不會接收到點選事件)。緊接著,事件在div上發生,並把事件真正的處理看成是冒泡階段的一部分,然後,冒泡階段發生,事件又回傳到document。

測試程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DOM2級事件流</title>
</head>
<style type="text/css">
    #box1{
        width: 300px;
        height: 300px;
        background-color: red;
    }
    #box2{
        width: 200px;
        height: 200px;
        background-color: yellow;
    }
    #box3{
        height: 100px;
        width: 100px;
        background-color: green;
    }
</style>
<body>
    <div id="box1">
        <div id="box2">
            <div id="box3"></div>
        </div>
    </div>
</body>
<script type="text/javascript">
    var box1 = document.getElementById('box1');
    var box2 = document.getElementById('box2');
    var box3 = document.getElementById('box3');
    // true表示在捕獲階段處理事件、false表示在冒泡階段處理
    box1.addEventListener('click',function () {
        console.log('事件捕獲階段觸發box1點選事件');
    }, true);
    box1.addEventListener('click',function () {
        console.log('事件冒泡階段觸發box1點選事件');
    }, false);
    box2.addEventListener('click',function () {
        console.log('事件捕獲階段觸發box2點選事件');
    }, true);
    box2.addEventListener('click',function () {
        console.log('事件冒泡階段觸發box2點選事件');
    }, false)
    box3.addEventListener('click',function () {
        console.log('事件捕獲階段觸發box3點選事件');
    }, true);
    box3.addEventListener('click',function () {
        console.log('事件冒泡階段觸發box3點選事件');
    }, false)
</script>
</html>

測試結果

圖片描述

4、事件流的典型應用——事件代理

傳統的事件處理中,需要為每個元素新增事件處理器。js事件代理則是一種簡單有效的技巧,透過它可以把事件處理器新增到一個父級元素上,從而避免把事件處理器新增到多個子級元素上。

事件代理的原理用到的就是事件冒泡和目標元素,把事件處理器新增到父元素,等待子元素事件冒泡,並且父元素能夠透過target(IE為srcElement)判斷是哪個子元素,從而做相應處理, 下面舉例說明:

傳統的事件會為每個dom新增事件,程式碼如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>傳統的事件繫結</title>
</head>

<body>
    <ul id="color-list">
        <li>red</li>
        <li>orange</li>
        <li>yellow</li>
        <li>green</li>
        <li>blue</li>
        <li>indigo</li>
        <li>purple</li>
    </ul>
</body>
<script>
(function() {
    var colorList = document.getElementById("color-list");
    var colors = colorList.getElementsByTagName("li");
    for (var i = 0; i < colors.length; i++) {
        colors[i].addEventListener('click', showColor, false);
    };

    function showColor(e) {
        e = e || window.event;
        var targetElement = e.target || e.srcElement;
        console.log(targetElement.innerHTML);
    }
})();
</script>
</script>

</html>

事件代理的處理方式如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>傳統的事件繫結</title>
</head>

<body>
    <ul id="color-list">
        <li>red</li>
        <li>orange</li>
        <li>yellow</li>
        <li>green</li>
        <li>blue</li>
        <li>indigo</li>
        <li>purple</li>
    </ul>
    <script>
    (function() {
        var colorList = document.getElementById("color-list");
        colorList.addEventListener('click', showColor, false);

        function showColor(e) {
            e = e || window.event;
            var targetElement = e.target || e.srcElement;
            if (targetElement.nodeName.toLowerCase() === "li") {
                alert(targetElement.innerHTML);
            }
        }
    })();
    </script>
</body>

</html>

使用事件代理的好處:

  • 將多個事件處理器減少到一個,因為事件處理器要駐留記憶體,這樣就提高了效能。想象如果有一個100行的表格,對比傳統的為每個單元格繫結事件處理器的方式和事件代理(即table上新增一個事件處理器),不難得出結論,事件代理確實避免了一些潛在的風險,提高了效能。

  • DOM更新無需重新繫結事件處理器,因為事件代理對不同子元素可採用不同處理方法。如果新增其他子元素(a,span,div等),直接修改事件代理的事件處理函式即可,不需要重新繫結處理器,不需要再次迴圈遍歷。

相關文章