深刻理解JavaScript 事件

事件

我的認爲:不管是瀏覽器自帶的事件,仍是自定義事件,都是觀察者模式的實現。更確切地說:事件流是會流動的,流到哪一個節點,事件在哪裏發生,事件發生時,節點便會調用在這個節點綁定的事件處理程序。節點是被觀察者,事件處理程序是觀察者,當事件流流到被觀察者時,被觀察者會對外宣稱「我這裏發生了某個事件」,即通知觀察者,也就是節點調用事件處理程序。事件流是不知道被觀察者有多少個的,因此即便是0個,事件流也會繼續流,流到節點時,節點會遍歷本身註冊的事件處理程序,存在就調用。具體瀏覽器的實現和優化確定更加複雜和精妙,但原理應該是這樣(以上爲我的理解)。javascript

事件流

事件流分爲事件冒泡和事件捕獲:css

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

事件冒泡

  1. 事件開始時由最具體的元素(文檔中嵌套層次最深的那個節點)接收,而後逐級向上傳播到較爲不具體的節點,全部現代瀏覽器都支持事件冒泡,除IE5.5外,均一直冒泡到window。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>事件流</title>
</head>
<body>
    <div id="box">Click me</div>
</body>
</html>
  1. IE的事件流稱爲事件冒泡。
  2. 即:事件由最具體的元素接收(div),逐級向上傳播到不具體的節點(document)
  3.  當咱們點擊代碼片斷一中id爲box的div塊時,單擊事件會按照以下順序傳播:
  4. div ——> body——> html ——> document

如上圖所示,click首先在div元素上發生,而後沿着Dom樹向上傳播,每一級節點都會發生直至傳播到document對象。html

測試代碼java

<!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>

測試效果:node

 

note: 幾乎現代全部的瀏覽器都支持事件冒泡,不過有一些細微的差異
IE5.5 和 IE5.5 - 版本的事件冒泡會跳過html元素(body 直接到 document)
IE九、Firefox、Chrome、Safari則一直冒泡到window對象。瀏覽器

事件捕獲

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

當咱們點擊代碼片斷一中id爲box的div塊時,單擊事件會按照以下順序傳播:
document——> html ——> body ——> div函數

note: 雖然事件捕獲是Netscape惟一支持的事件流模型,但IE九、Firefox、Chrome、Safari目前也都支持這種事件模型,因爲老版本的瀏覽器並不支持,因此咱們應該儘可能使用事件冒泡,有特殊需求的時候再考慮事件捕獲。性能

DOM2級事件流

爲了可以兼容上述兩種事件模型,又提出了一個DOM2級事件模型,它規定了事件流包含三個階段:測試

  1. 事件捕獲階段:爲事件捕獲提供機會;

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

  3. 事件冒泡階段:事件響應階段;

<!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>

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

傳統的事件處理中,須要爲每一個元素添加事件處理器。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>

使用事件代理的好處:

  1. 將多個事件處理器減小到一個,由於事件處理器要駐留內存,這樣就提升了性能。想象若是有一個100行的表格,對比傳統的爲每一個單元格綁定事件處理器的方式和事件代理(即table上添加一個事件處理器),不可貴出結論,事件代理確實避免了一些潛在的風險,提升了性能。

  2. DOM更新無需從新綁定事件處理器,由於事件代理對不一樣子元素可採用不一樣處理方法。若是新增其餘子元素(a,span,div等),直接修改事件代理的事件處理函數便可,不須要從新綁定處理器,不須要再次循環遍歷。

 

跨瀏覽器的事件對象

var EventUtil = {
    getEvent: function(event){
        return event ? event : window.event; // window.event DOM0級時IE
    },
    getTarget: function(event){
        return event.target || event.srcElement; // event.srcElement for IE
    },
    preventDefault: function(event){
        if (event.preventDefault){
            event.preventDefault();
        } else {
            event.returnValue = false; // IE
        }
    },
    stopPropagation: function(event){
        if (event.stopPropagation){
            event.stopPropagation();
        } else {
            event.cancelBubble = true; // IE
        }
    }
};

var EventUtil = {
    addHandler: function(element, type, handler){
        if (element.addEventListener){
            element.addEventListener(type, handler, false);
        } else if (element.attachEvent){
            element.attachEvent("on" + type, handler);
        } else {
            element["on" + type] = handler;
        }
    },
    removeHandler: function(element, type, handler){
        if (element.removeEventListener){
            element.removeEventListener(type, handler, false);
        } else if (element.detachEvent){
            element.detachEvent("on" + type, handler);
        } else {
            element["on" + type] = null;
        }
    }
};

 

事件委託

<body>
    <ul id="myLinks">
        <li id="goSomewhere">Go somewhere</li>
        <li id="doSomething">Do something</li>
        <li id="sayHi">Say hi</li>
    </ul>
    <script type="text/javascript">
    (function(){
        var list = document.getElementById("myLinks");

        EventUtil.addHandler(list, "click", function(event){
            event = EventUtil.getEvent(event);
            var target = EventUtil.getTarget(event);

            switch(target.id){
                case "doSomething":
                    document.title = "I changed the document's title";
                    break;

                case "goSomewhere":
                    location.href = "http://www.wrox.com";
                    break;

                case "sayHi":
                    alert("hi");
                    break;
            }
        });

    })();
    </script>
</body>
  • 上面的方法只取得了一個 DOM 元素,只添加了一個事件處理程序,佔用的內存更少。
  • 若是將事件委託到 document 中,會更有優點:
  1. document 對象很快就能夠訪問,並且能夠在頁面生命週期的任什麼時候點上爲它添加事件處理程序(無需等待 DOMContentLoaded 或 load 事件)。
  2. 在頁面中設置事件處理程序所需的時間少。只添加一個事件處理程序所需的 DOM 引用更少,所花的時間也更少。
  3. 整個頁面佔用的內存空間更少,可以提高總體性能。
  • 最適合採用事件委託技術的事件包塊 clickmousedownmouseupkeydownkeyup 和 keypress
相關文章
相關標籤/搜索