瀏覽器事件機制

事件機制

無論是前端還是移動端,用戶在瀏覽網頁或者APP時,通常會在屏幕上產生很多交互操作,例如點擊、選擇、滾動屏幕、鍵盤輸入等待,並且網頁或APP也會根據不同的操作進行響應變化。這種基於事件的處理方式,本質上是一種消息傳遞機制,稱之爲事件機制。

在事件機制中,有3樣最重要的東西:

  • 事件生產者
  • 事件對象
  • 事件消費者

事件生產者可以產生一系列的事件對象,然後事件對象攜帶着必要的信息,傳遞給事件消費者。

上圖所示是一種單向的消息傳遞模型,事件消息總是由事件生產者傳遞給事件消費者。而如果要使得事件生產者和事件消費者形成雙向通信,那麼很簡單,讓兩者同時作爲事件生產者和事件消費者就可以了。

然後呢,一個事件可以傳遞給多個接受對象,即一個事件生產者產生的事件可以對應有多個事件消費者:

相反地,一個事件消費者也可以接受多個事件生產者產生的事件消息:

一、事件流及事件綁定

EMCAScript標準規定事件流包含三個階段,分別爲事件捕獲階段,處於目標階段,事件冒泡階段。

<html>
    <body>
        <div>
            <button id="mybtn" onclick="buttonClickHandler(event)">點我試試</button>
        </div>
    </body>
</html>
<script>
    function buttonClickHandler(event) {
        console.log('button clicked')
    }
</script>

在上面的代碼中,如果點擊按鈕button,則標準事件觸發分別經歷以下三個階段:

preview

在W3C模型中,任何事件發生時,先從頂層開始進行事件捕獲,直到事件觸發到達了事件源元素,這個過程叫做事件捕獲(這其實也是事件的傳遞過程);然後,該事件會隨着DOM樹的層級路徑,由子節點向父節點進行層層傳遞,直至到達document,這個過程叫做事件冒泡(也可以說這是事件的響應過程)。雖然大部分的瀏覽器都遵循着標準,但是在IE瀏覽器中,事件流卻是非標準的。而IE中事件流只有兩個階段:處於目標階段,冒泡階段。 

下面看一個Chrome瀏覽器中的例子

<html>
<head>
<style>
ul{
    background : gray;
    padding : 20px;
}
ul li{
    background : green;
}
</style>
</head>
<body>
<ul>
    <li>點我試試</li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
var li = document.getElementsByTagName('li')[0];
document.addEventListener('click',function(e){console.log('document clicked')},true);//第三個參數爲true使用捕獲
ul.addEventListener('click',function(e){console.log('ul clicked')},true);
li.addEventListener('click',function(e){console.log('li clicked')},true);
</script>
</body>
</html>

以上代碼中,我們創建了一個列表項,點擊“點我試試”,看看會有什麼情況發生:

document clicked
ul clicked
li clicked

在我們的開發者工具控制檯上,可以看到打印出了這樣三行結果,這是我們預料之中的事情,因爲在這裏事件捕獲起了作用,點擊事件依次觸發了document、ul節點、li節點。

而在IE中只支持冒泡機制,所以只能在冒泡階段進行事件綁定以及事件撤銷:

target.attachEvent(type, listener);  //target: 文檔節點、document、window 或 XMLHttpRequest。
                                     //函數參數: type:註冊事件類型;
                                     //         listener:事件觸發時的回調函數。
target.detachEvent(type,listener);   //參數與註冊參數相對應。

下面看一個IE瀏覽器裏的例子:

<html>
<body>
<ul>
    <li>點我試試</li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
var li = document.getElementsByTagName('li')[0];
document.attachEvent('onclick',function(event){console.log('document clicked')})
ul.attachEvent('onclick',function(event){console.log('ul clicked')});
li.attachEvent('onclick',function(event){console.log('li clicked')});
</script>
</body>
</html>

同樣地,我們點擊“點我試試”,開發者工具控制檯裏打印出了下面的結果:

li clicked
ul clicked
document clicked

然而有時候事件的捕獲機制以及冒泡機制也會帶來副作用,比如冒泡機制會觸發父節點上原本並不希望被觸發的監聽函數,所以有辦法可以使得冒泡提前結束嗎?我們只需要在希望事件停止冒泡的位置,調用event對象的stopPropagation函數(IE瀏覽器中爲cancelBubble)即可終止事件冒泡了。比如在上面IE瀏覽器中示例代碼作如下修改:

li.attachEvent('onclick',function(event){
    console.log('li clicked');
    event.cancelBubble=true;
});

修改後,再次點擊“點我試試”,在控制檯裏只打印出一行結果,ul節點和document不會再接收到冒泡上來的click事件,因而它們註冊的事件處理函數也將不會被觸發了:

li clicked

二、事件委託

什麼是事件委託呢
事件委託就是利用事件冒泡機制,指定一個事件處理程序,來管理某一類型的所有事件。這個事件委託的定義不夠簡單明瞭,可能有些人還是無法明白事件委託到底是啥玩意。查了網上很多大牛在講解事件委託的時候都用到了取快遞這個例子來解釋事件委託,不過想想這個例子真的是相當恰當和形象的,所以就直接拿這個例子來解釋一下事件委託到底是什麼意思:
公司的員工們經常會收到快遞。爲了方便籤收快遞,有兩種辦法:一種是快遞到了之後收件人各自去拿快遞;另一種是委託前臺MM代爲簽收,前臺MM收到快遞後會按照要求進行簽收。很顯然,第二種方案更爲方便高效,同時這種方案還有一種優勢,那就是即使有新員工入職,前臺的MM都可以代替新員工簽收快遞。
這個例子之所以非常恰當形象,是因爲這個例子包含了委託的兩層意思:
首先,現在公司裏的員工可以委託前臺MM代爲簽收快遞,即程序中現有的dom節點是有事件的並可以進行事件委託;其次,新入職的新員工也可以讓前臺MM代爲簽收快遞,即程序中新添加的dom節點也是有事件的,並且也能委託處理事件。

爲什麼要用事件委託呢
當dom需要處理事件時,我們可以直接給dom添加事件處理程序,那麼當許多dom都需要處理事件呢?比如一個ul中有100li,每個li都需要處理click事件,那我們可以遍歷所有li,給它們添加事件處理程序,但是這樣做會有什麼影響呢?我們知道添加到頁面上的事件處理程序的數量將直接影響到頁面的整體運行性能,因爲這需要不停地與dom節點進行交互,訪問dom的次數越多,引起瀏覽器重繪和重排的次數就越多,自然會延長頁面的交互就緒時間,這也是爲什麼可以減少dom操作來優化頁面的運行性能;而如果使用委託,我們可以將事件的操作統一放在js代碼裏,這樣與dom的操作就可以減少到一次,大大減少與dom節點的交互次數提高性能。同時,將事件的操作進行統一管理也能節約內存,因爲每個js函數都是一個對象,自然就會佔用內存,給dom節點添加的事件處理程序越多,對象越多,佔用的內存也就越多;而使用委託,我們就可以只在dom節點的父級添加事件處理程序,那麼自然也就節省了很多內存,性能也更好。
事件委託怎麼實現呢?因爲冒泡機制,既然點擊子元素時,也會觸發父元素的點擊事件。那麼我們就可以把點擊子元素的事件要做的事情,交給最外層的父元素來做,讓事件冒泡到最外層的dom節點上觸發事件處理程序,這就是事件委託。
在介紹事件委託的方法之前,我們先來看看處理事件的一般方法

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var item1 = document.getElementById("item1");
var item2 = document.getElementById("item2");
var item3 = document.getElementById("item3");

item1.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item1");
}
item2.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item2");
}
item3.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item3");
}
</script>

上面的代碼意思很簡單,就是給列表中每個li節點綁定點擊事件,點擊li的時候,需要找一次目標li的位置,執行事件處理函數。

那麼我們用事件委託的方式會怎麼做呢(查看示例)?

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var item1 = document.getElementById("item1");
var item2 = document.getElementById("item2");
var item3 = document.getElementById("item3");
var list = document.getElementById("list");
list.addEventListener("click",function(event){
 var target = event.target;
 if(target == item1){
    alert(event.target.nodeName);
    console.log("hello item1");
 }else if(target == item2){
    alert(event.target.nodeName);
    console.log("hello item2");
 }else if(target == item3){
    alert(event.target.nodeName);
    console.log("hello item3");
 }
});
</script>

我們爲父節點添加一個click事件,當子節點被點擊的時候,click事件會從子節點開始向上冒泡。父節點捕獲到事件之後,通過判斷event.target來判斷是否爲我們需要處理的節點, 從而可以獲取到相應的信息,並作處理。很顯然,使用事件委託的方法可以極大地降低代碼的複雜度,同時減小出錯的可能性。

我們再來看看當我們動態地添加dom時,使用事件委託會帶來哪些優勢?首先我們看看正常寫法

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var list = document.getElementById("list");

var item = list.getElementsByTagName("li");
for(var i=0;i<item.length;i++){
    (function(i){
        item[i].onclick = function(){
            alert(item[i].innerHTML);
        }
    })(i);
}

var node=document.createElement("li");
var textnode=document.createTextNode("item4");
node.appendChild(textnode);
list.appendChild(node);

</script>

點擊item1到item3都有事件響應,但是點擊item4時,沒有事件響應。說明傳統的事件綁定無法對動態添加的元素而動態的添加事件。

而如果使用事件委託的方法又會怎樣呢(查看示例)?

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var list = document.getElementById("list");

document.addEventListener("click",function(event){
    var target = event.target;
    if(target.nodeName == "LI"){
        alert(target.innerHTML);
    }
});

var node=document.createElement("li");
var textnode=document.createTextNode("item4");
node.appendChild(textnode);
list.appendChild(node);

</script>

當點擊item4時,item4有事件響應,這說明事件委託可以爲新添加的DOM元素動態地添加事件。我們可以發現,當用事件委託的時候,根本就不需要去遍歷元素的子節點,只需要給父級元素添加事件就好了,其他的都是在js裏面的執行,這樣可以大大地減少dom操作,這就是事件委託的精髓所在。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章