原文地址

什麼是事件流

:用術語說流是對輸入輸出裝置的抽象。以程式的角度說,流是具有方向的資料。

事件流:從頁面中接收事件的順序。也就是說當一個事件產生時,這個事件的傳播過程,就是事件流。

事件:使用者或者瀏覽器自身執行的某個動作,比如load,click,mousemove等

事件處理程式:相應處理某個事件的函式叫做事件處理函式(也叫做事件偵聽器)

比如說React中的單向資料流,Node中的流,又或是今天本文所講的DOM事件流。都是流的一種生動體現。

理解DOM中的事件流

當瀏覽器發展到第四代時(IE4和Netscape Communicator 4),瀏覽器團隊遇到一個很有意思的問題:頁面的哪一部分會擁有特定的事件?想象下在一張紙上有一組同心圓,如果你把手指放在圓心上,那麼你的手指指向的不是一個圓,而是一組圓。兩家公司的開發團隊在看待瀏覽器事件方面還是一致的。如果你單擊了某個按鈕,那麼同時你也單擊了按鈕的容器元素,甚至整個頁面。
事件流描述的是從頁面中接受事件的順序。但有意思的是,IE和Netscape開發團隊居然提出了兩個截然相反的事件流概念。IE的事件流是事件冒泡流,而Netscape的事件流是事件捕獲流。

IE提出的事件冒泡

事件冒泡即事件開始時,由最具體的元素接收(也就是事件發生所在的節點),然後逐級傳播到較為不具體的節點。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <button id="click">點選</button>
    <script>
        (function(){
            var btn = document.getElementById("click");
            btn.onclick = function(){
                console.log("1. button");
            }
            document.body.onclick = function(){
                console.log("2. document.body");
            }
            document.onclick = function(){
                console.log("3. document");
            }
            window.onclick = function(){
                console.log("4. window");
            }
        })()
    </script>
</body>
</html>

在程式碼所示的頁面中,如果點選了button,那麼這個點選事件會得到如下的結果:
程式碼結果截圖
也就是說,click事件首先在button元素上發生,然後逐級向上傳播。這就是事件冒泡。

netscape提出的事件捕獲

事件捕獲的概念,與事件冒泡正好相反。它認為當某個事件發生時,父元素應該更早接收到事件,具體元素則最後接收到事件。比如說剛才的demo,如果是事件捕獲的話,事件發生順序會是剛好與上面相反的。即window,document,document.body,button。

雖然事件捕獲是Netscape唯一支援的事件流模型,但IE9、Safari、Chrome、Opera和Firefox目前也都支援這種事件流模型。但由於老版本的瀏覽器不支援,因此很少有人使用事件捕獲。

所以放心的使用事件冒泡,有特殊需要再使用事件捕獲即可。

DOM事件流

DOM事件流可以分為下面3個階段:
1. 事件捕獲階段
2. 處於目標階段
3. 事件冒泡階段
dom事件

事件捕獲階段

也就是說,當事件發生時,首先發生的是事件捕獲,為父元素截獲事件提供了機會。
例如,我把上面的Demo中,window點選事件更改為使用事件捕獲模式。

addEventListener最後一個引數,為true則代表使用事件捕獲模式,false則表示使用事件冒泡模式。

<script>
    (function(){
        var btn = document.getElementById("click");
        btn.addEventListener("click",function(){
            console.log("1. button");
        },true)
        //省略document.body和document
       .....
        window.addEventListener("click",function(){
            console.log("4. window");
        },true)
    })()
    </script>

結果如下:

可以看到,點選事件先被父元素截獲了,且該函式只在事件捕獲階段起作用。

在DOM事件流中,事件的目標在捕獲階段不會接受到事件。這意味著在捕獲階段,事件從document到body後就定停止了。下一個階段是處於目標階段,於是事件在button上發生,並在事件處理中被看成冒泡階段的一部分。然後,冒泡階段發生,事件又傳播回document。

但是:我們的各大瀏覽器總是不喜歡按照規範來,IE9,Safari,chrome,firefox及其更高的版本中都會在捕獲階段出發事件物件上的事件,最後導致有兩個機會在目標物件上操作事件。

處於目標與事件冒泡階段

事件到了具體元素時,在具體元素上發生,並且被看成冒泡階段的一部分。
隨後,冒泡階段發生,事件開始冒泡。

阻止事件冒泡

件冒泡過程,是可以被阻止的。防止事件冒泡而帶來不必要的錯誤和困擾。

這個方法就是:stopPropagation()

(function(){
    var btn = document.getElementById("click");
    btn.addEventListener("click",function(event){
        console.log("1. button");
        event.stopPropagation();
        console.log('Stop Propagation!');
    },false)
    //省略document.body和document
       .....
    window.addEventListener("click",function(){
        console.log("4. window");
    },false)
})()

最後結果是:1.button,Stop Propagation!。通過stopPropagation();阻止了事件的冒泡。

事件處理程式類別

剛剛我們已經講了事件處理程式就是相應處理某個事假的函式。它可以分為幾個類別:

html事件處理程式

某個元素支援的某個事件可以用與事件處理程式同名的html特性來指定,該特性的值是能夠執行的javascript程式碼,這也是我們最初學js,最開始的方法。

<script>
    function show(){
        alert('我被點選了');
    }
/*
  點選後也會彈出 '我被點選了'
*/
</script>
<input type="button" value="點選" onclick="show()" />

優點:簡單明瞭,省去獲取元素等一系列前提操作

缺點:html程式碼與js程式碼高度耦合,不符合分離原則

DOM0級別事件處理函式

DOM0級別事件處理函式,使用 element.on[eventname]=fn的方式給元素新增事件

<input type="button" value="點選" id="click" />
<script>
    var oBtn=document.getElementById('click');
        //該方式被認為是元素的方法,即事件處理程式在元素的作用域中進行,this即該元素本身
        oBtn.onclick=function(){
            alert(this.id);//click
        }
        //注意:刪除該事件處理程式可以用如下方法
        oBtn.onclick=null;//即點選後不再有任何反應       
</script>

DOM2級事件處理程式

DOM2級添加了addEventListener(新增事件處理程式)和removeEventListener(移除事件處理程式),也就是我們剛剛講的上面的DOM2例子。

新增事件處理函式addEventListener

引數1 指定事件名稱...click mouseover mouseout
引數2 事件處理程式(匿名函式或者有名函式)
引數3 true(捕獲階段發生) or false(冒泡階段發生)
<input type="button" value="點選" id="click" />
<script>
    var oBtn=document.getElementById('click');
    oBtn.addEventListener('click',function(){
        alert(this.id)//click  this指的是該元素作用域內
    },false)
    //注意該種方式可以給一個函式新增多個事件處理函式,執行順序與新增順序相同
    oBtn.addEventListener('click',function(){
        alert('Hello World')//click
    },false)         
</script>

移除事件處理函式removeEventListener

如果事件處理函式是有名函式,則可以通過名字來移除,匿名函式無法移除。

<input type="button" value="點選" id="click" />
<script>
 var oBtn=document.getElementById('click');
function showId(){
    alert(this.id);
};
function HelloWorld(){
    alert('HellowWorld');
}
oBtn.addEventListener("click",showId,false);
oBtn.addEventListener("click",HelloWorld,false);
oBtn.removeEventListener('click',showId,false)
</script>     

最後只能彈出HellowWorld

IE事件處理程式attachEvent,detachEvent
ie實現了與dom類似的兩個方法,attachEvent(新增),detachEvent(刪除)

 oBtn.attachEvent('onclick',showId);//這時候會報錯,因為這裡的是在window的作用域內
//修改如下
oBtn.detachEvent('onclick',showId) ;//點選沒有任何反應