1. 程式人生 > >js學習筆記:事件——事件流、事件處理程式、事件物件

js學習筆記:事件——事件流、事件處理程式、事件物件

Javascript與HTML之間的互動是通過事件實現的。
事件,就是文件或瀏覽器視窗中發生的一些特定的互動瞬間。
可以使用偵聽器來預定事件,以便事件發生時執行相應程式碼。

事件流

事件流描述的是從頁面中接受事件的順序。

事件冒泡

IE的事件流叫做事件冒泡,即事件開始是由最具體的元素接收,然後逐級向上傳播到較為不具體的節點(文件)。
如果點選了頁面中的一個div元素,那麼這個click事件可能會按照如下順序傳播:

  • < div>
  • < body>
  • < html>
  • document

也就是說,click事件首先在div元素上發生,然後click事件沿DOM樹向上傳播

,在每一級節點上都會發生,直至傳播至document物件。

這裡寫圖片描述

所有現代瀏覽器都支援冒泡事件。

事件捕獲

Netscape團隊提出的另一種事件流叫做事件捕獲。事件捕獲的思想是不太具體的節點應該更早接收到事件,最具體的節點應該最後接收到事件。
事件捕獲的用意在於事件到達預定目標之前捕獲它。

這時,單擊div元素就會以下列順序觸發click事件:

  • document
  • < html>
  • < body>
  • < div>

事件捕獲過程中,document物件首先接受click事件,然後事件沿DOM樹依次向下,一直傳播到事件的實際目標,即div元素。

這裡寫圖片描述

雖然大多數瀏覽器都支援事件捕獲,但很少有人使用。

DOM事件流

DOM2級事件規定的事件流包括三個階段:事件捕獲階段、處於目標階段、事件冒泡階段。

  • 首先發生的是事件捕獲,為截獲事件提供了機會。
  • 然後是實際的目標接收到事件。
  • 然後冒泡階段發生,事件又傳播迴文檔。

這裡寫圖片描述

在DOM事件流中,實際的目標在捕獲階段不會接受到事件。這意味著在捕獲階段,事件從document到< html>再到< body>後就停止了。
下一個階段是“處於目標”階段,於是事件在div元素上發生,並在事件處理中被看成冒泡階段的一部分。

即使DOM2級事件明確要求捕獲階段不會涉及事件目標,但大多數瀏覽器都會在捕獲階段觸發事件物件上的事件

,結果就是有兩個機會在目標物件上面操作事件。

事件處理程式

響應某個事件的函式就叫做事件處理程式(或事件偵聽器)。
事件處理程式的名字以“on”開頭,因此click事件的事件處理程式就是onclick,load事件的事件處理程式就是onload。
為事件指定處理程式的方式有好幾種。

HTML事件處理程式

某個元素支援的每種事件,都可以使用一個與相應事件處理程式同名的HTML特性來指定。這個特性的值應該是能夠執行的javascript程式碼。

<input type="button" value="click me" onclick="alert('clicked')"/>

當單擊這個按鈕時,就會顯示一個警告框。這個操作是通過指定onclick特性並將一些javascript程式碼作為它的值來定義的。由於這個值是javascript,因此不能在其中使用未經轉義的HTML語法字元。

在HTML中定義的事件處理程式可以包含要執行的具體動作,也可以呼叫在頁面其他地方定義的指令碼。

<script type="text/javascript">
    function showMessage(){
        alert("hello world")
    }
</script>
<input type="button" value="click me" onclick="showMessage()" />

在這個例子中,單擊按鈕就會呼叫showMessage函式。這個函式是在一個獨立的< script>元素中定義的,當然也可以被包含在一個外部檔案中。事件處理程式中的程式碼在執行時,有權訪問全域性作用域中的任何程式碼。

這樣指定事件處理程式具有一些獨到之處。
首先,這樣會建立一個封裝著元素屬性值的函式。
這個函式中有一個區域性變數event,也就是事件物件。

<input type="button" value="click me" onclick="alert(event.type)" />

通過event變數,可以直接訪問事件物件。

在這個函式內部,this值等於事件的目標元素。

<input type="button" value="click me" onclick="alert(this.value)" />

關於這個動態建立的函式,另一個有意思的地方就是它擴充套件作用域的方式。這個函式使用with像下面這樣擴充套件作用域:

function(){
    with(document){
        with(this){
            //元素屬性值
        }
    }
}

如此一來,事件處理程式可以直接訪問自己的屬性:

<input type="button" value="click me" onclick="alert(value)" />

如果當前元素是一個表單輸入元素,則作用域中還會包含訪問表單元素的入口。

function(){
    with(document){
        with(this.form){
            with(this){
                //元素屬性值
            }
        }
    }
}

這樣的擴充套件方式,無非就是想讓事件處理程式無需引用表單元素就能訪問其他表單欄位。

<form method="post">
    <input type="text" name="username" value="">
    <input type="button" value="echo username" onclick="alert(username.value)">
</form>

注意,這個例子中直接引用了username元素。

但是,在HTML中指定事件處理程式有兩個缺點:

  • 存在一個時差問題。使用者可能在HTML元素一出現在頁面上就觸發相應的事件,但當時的事件處理程式有可能尚不具備執行條件。
    以前面的例子來說明,假設showMessage函式是在按鈕下方,頁面的最底部定義的。如果使用者在頁面解析showMessage( )之前就單擊了按鈕,就會引發錯誤。為此,很多HTML事件處理程式都會被封裝到一個try-catch塊中,一遍錯誤不會浮出水面。

  • 這樣擴充套件事件處理程式的作用域鏈在不同瀏覽器中會導致不同結果。

  • HTML程式碼與javascript程式碼緊密耦合。如果要更換事件處理程式,就要改動兩個地方:HTML程式碼和javascript程式碼。這正是大家摒棄HTML事件處理程式,轉而使用javascript指定事件處理程式的原因所在。

DOM0級事件處理程式

通過javascript指定事件處理程式的傳統方式,就是將一個函式賦值給一個事件處理程式屬性。
每個元素(包括window和document)都有自己的事件處理程式屬性,這些屬性通常全部小寫,例如onclick。將這種屬性的值設定為一個函式,就可以指定事件處理程式:

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    alert("clicked");
}

在此,我們通過文件物件取得了一個按鈕的引用,然後為它指定了onclick事件處理程式。
但要注意,在這些程式碼執行以前不會指定事件處理程式,因此如果這些程式碼在頁面中位於按鈕後面,就有可能在一段時間內怎麼單擊都沒有反應。

使用DOM0級方法指定的事件處理程式被認為是元素的方法。因此,這時候的事件處理程式是在元素的作用域中執行;換句話說,程式中的this引用當前元素。

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    alert(this.id);  //"myBtn"
}

不僅僅是id,實際上可以在事件處理程式中通過this訪問元素的任何屬性和方法。
以這種方式新增的事件處理程式會在事件流的冒泡階段被處理。

也可以刪除通過DOM0級方法指定的事件處理程式,只要像下面這樣將事件處理程式屬性的值設定為null即可:

btn.onclick = null;  //刪除事件處理程式

將事件處理程式設定為null後,再單擊按鈕將不會有任何動作發生。

如果使用HTML指定事件處理程式,那麼onclick屬性的值就是一個包含著在同名HTML特性中指定的程式碼的函式。將相應的屬性設為null,也可以刪除以這種方式指定的事件處理程式。

DOM2級事件處理程式

DOM2級事件定義了2個方法:

  • addEventListener()
  • removeEventListener()

所有DOM節點中都包含這兩個方法,並且它們都接受3個引數:

  • 要處理的事件名
  • 作為事件處理程式的函式
  • 一個布林值
    • true:表示在捕獲階段呼叫事件處理程式
    • false:表示在冒泡階段呼叫事件處理程式

要在按鈕上為click事件新增事件處理程式,可以使用下列程式碼:

var btn = document.getElementById("myBtn");
btn.addEventListener("click",function(){
    alert(this.id);
},false);

上面的程式碼為一個按鈕添加了onclick事件處理程式,而且該事件會在冒泡階段被觸發。
與DOM0級方法一樣,這裡新增的事件處理程式也是在其依附的元素的作用域中執行,因此this代表該元素。

使用DOM2級方法新增事件處理程式的主要好處是可以新增多個事件處理程式。

btn.addEventListener("click",function(){
    alert(this.id);
},false);
btn.addEventListener("click",function(){
    alert("hello world");
},false);

這裡為按鈕添加了兩個事件處理程式,這兩個事件處理程式會按照新增他們的順序觸發,因此首先會顯示元素的ID,其次會顯示“hello world”訊息。

通過addEventListener()新增的事件處理程式只能使用removeEventListener()來移除;移除時傳入的引數與新增處理程式時使用的引數相同。這也意味著通過addEventListener()新增的匿名函式將無法移除。

btn.addEventListener("click",function(){
    alert(this.id);
},false);

btn.removeEventListener("click",function(){ //沒有用
    alert(this.id);
},false);

這個例子中,在移除事件處理程式時,看似使用了相同的引數,但實際上第二個引數與傳入addEventListener()中的是完全不同的函式
傳入removeEventListener()中的事件處理程式函式必須與傳入addEventListener()中的完全相同

var handler = function(){
    alert(this.id);
}
btn.addEventListener("click",handler false);

btn.removeEventListener("click",handler ,false); //有效!

重寫後的這個列子可以奏效,因為在addEventListener()和removeEventListener()中使用了相同的函式。

大多數情況下,都是將事件處理程式新增到事件流的冒泡階段,這樣可以最大限度的相容各種瀏覽器。最好只在需要在事件到達目標之前截獲他的時候將事件處理程式新增到捕獲階段。如果不是特別需要,不建議在事件捕獲階段註冊時間處理程式。

IE事件處理程式

IE實現了與DOM中類似的鏈各個方法:attachEvent()和detachEvent()。
這兩個方法接受相同的兩個引數,事件處理程式名稱與事件處理程式函式。
由於IE8及更早版本只支援事件冒泡,所以通過attachEvent()新增的事件處理程式都會被新增到冒泡階段。

要使用attachEvent()為按鈕新增一個事件處理程式,可以使用以下程式碼:

btn.attachEvent("onclick",function(){
    alert("hello world");
});

注意,attachEvent()的第一個引數是“onclick”,而非DOM方法中的“click”。

在IE中使用attachEvent()與使用DOM0級方法的主要區別在於事件處理程式的作用域。在使用DOM0級方法的情況下,事件處理程式會在其所屬元素的作用域內執行。在使用attachEvent()方法的情況下,事件處理程式會在全域性作用域中執行,因此this等於window。

btn.attachEvent("onclick",function(){
    alert(this == window); //true
});

與addEventListener()類似,attachEvent()方法也可以用來為一個元素新增多個事件處理程式。

btn.attachEvent("onclick",function(){
    alert("clicked"); 
});
btn.attachEvent("onclick",function(){
    alert("hello world"); 
});

使用attachEvent為同一個按鈕添加了兩個不同的事件處理程式。但與DOM事件不同的是,這些事件處理程式不是以新增他們的順序執行,而是以相反的順序被觸發。 單擊這個按鈕,首先會看到“hello world”,然後才是“clicked”。

使用attachEvent()新增的事件可以通過detachEvent()來移除,條件是必須提供相同的引數。這也意味著新增的匿名函式將不能被移除。不過,只要能夠將對相同函式的引用傳給detachEvent(),就可以移除相應的事件處理程式。

var handler = function(){
    alert(this.id);
}
btn.attachEvent("onclick",handler );
btn.detachEvent("onclick",handler );

這個例子將儲存在變數handler中的函式作為事件處理程式。因此,後面的detachEvent()可以使用相同的函式來移除事件處理程式。

支援IE事件處理程式的瀏覽器有IE和opera。

跨瀏覽器的事件處理程式

可以自己編寫跨瀏覽器的事件處理程式,只要在適當的地方使用能力檢測即可。要保證處理事件的程式碼能在大多數瀏覽器下一致地執行,只需關心冒泡階段。
第一個要建立的方法是addHandler(),他的職責是視情況分別使用DOM0級方法、DOM2級方法或IE方法來新增事件。這個方法屬於一個名叫EventUtil的物件,我們將使用這個物件來處理瀏覽器間的差異。
addHandler接收3個引數:要操作的元素、事件名稱和事件處理函式。

與addHandler()對應的方法是removeHandler(),它也接受相同的引數。這個方法的職責是移除之前新增的事件處理程式——無論該事件處理程式是採取什麼方式新增到元素中的,如果其它方法無效,預設採用DOM0級方法。

var EventUtil = {
    addHandler:function(element,type,handler){
        if(element.addEventListener){
            element.addEventListner(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.removeEventListner(type,handler,false);
        }else if(element.detachEvent){
            element.detachEvent("on"+type,handler);
        }else{
            element["on"+type] = null;
        }
    }
}

這兩個方法首先都會檢測傳入的元素中是否存在DOM2級方法。如果存在DOM2級方法,則使用該方法;如果存在的是IE的方法,則採取第二種方案。最後一種可能就是DOM0級方法,此時我們使用的是方括號語法來將屬性名指定為事件處理程式,或者將屬性設定為null。

可以像下面這樣使用EventUtil物件:

var btn = document.getElementById("myBtn");
var handler = function(){
    alert(this.id);
}
EventUtil.addHandler(btn,"click",handler);
EventUtil.removeHandler(btn,"click",handler);

addHandler()和removeHandler()沒有考慮到所有的瀏覽器問題,例如IE中的作用域問題。不過,使用它們新增和移除事件處理程式還是足夠了。
此外還要注意,DOM0級對每個事件只支援一個事件處理程式。

事件物件

在觸發DOM上的某個事件時,會產生一個事件物件event,這個物件中包含著所有與事件有關的資訊。包括導致事件的元素、事件的型別以及其他與特定事件相關的資訊。
所有瀏覽器都支援event物件,但支援方式不同。

DOM中的事件物件

相容DOM的瀏覽器會將一個event物件傳入到事件處理程式中。無論指定事件處理程式時使用什麼方法(DOM0級或DOM2級),都會傳入event物件。

btn.onclick = function(event){
    alert(event.type);  //"click"
}
btn.addEventListener("click",function(event){
    alert(event.type);  //"click"
},false);

這個例子中的兩個事件處理程式都會彈出一個警告框,顯示由event.type屬性表示的事件型別。這個屬性始終都會包含被觸發的事件型別。(與傳入addEventListener()和removeEventListener()的事件型別一致)。

在通過HTML特性指定事件處理程式時,變數event中儲存著event物件。

<input type="button" value="click me" onclick="alert(event.type)" />

以這種方式提供event物件,可以讓HTML特性事件處理程式與javascript函式執行相同的操作。

event物件包含與建立它的特定事件有關的屬性和方法。觸發的事件型別不一樣,可用的屬性和方法也不一樣。

currentTarget、target

target在事件流的目標階段;currentTarget在事件流的捕獲,目標及冒泡階段。只有當事件流處在目標階段的時候,兩個的指向才是一樣的, 而當處於捕獲和冒泡階段的時候,target指向被單擊的物件而currentTarget指向當前事件活動的物件(一般為父級)。

換句話說,event.target指向引起觸發事件的元素,而event.currentTarget則是事件繫結的元素

在事件處理程式內部,物件this始終等於currentTarget的值,而target只包含事件的實際目標。如果直接將事件處理程式指定給了目標元素,則this、currentTarget、target包含相同的值。

btn.onclick = function(event){
    alert(event.currentTarget === this);  //true
    alert(event.target === this);  //true
}

由於click事件的目標是按鈕,因此這三個值是相等的。
如果事件處理程式存在於按鈕的父節點中,那麼這些值是不相同的:

document.body.onclick = function(event){
    alert(event.currentTarget === document.body);  //true
    alert(this === document.body);  //true
    alert(event.target === document.getElementById("myBtn"));  //true
}

當單擊這個例子中的按鈕時,this和currentTarget都等於document.body,因為事件處理程式是註冊到這個元素上的。然而target元素卻等於按鈕元素,因為它是click的真正目標。由於按鈕上沒有註冊事件處理程式,結果click事件就冒泡到了document.body上了,在那裡事件才得到了處理。

type

在需要通過一個函式處理多個事件時,可以使用type屬性。

var handler = function(event){
    switch(event.type){
        case "click":
            alert("clicked");
            break;
        case "mouseover":
            event.target.style,backgroundColor = "red";
            break;
        case "mouseout":
            event.target.style.backgroundColor = "";
            break;
    }
};

btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

這個例子定義了一個名為handler的函式,用於處理3種事件:click、mouseover、mouseout。函式中通過檢測event.type屬性,讓函式能夠確定發生了什麼事件,並執行相應的操作。

preventDefault()

要阻止特定事件的預設行為,可以使用這個方法。例如,連結的預設行為是被單擊時會導航到其href特性指定的URL。如果想阻止這一預設行為,那麼通過連線的onclick事件處理程式可以取消它。

link.onclick = function(event){
    event.preventDefault();
}

只有cancelable屬性設為true的事件,才可以使用preventDefault()來取消其預設行為。

stopPropagation()

用於立即停止事件在DOM層次中的傳播,即取消進一步的事件捕獲或冒泡。
例如,直接新增到一個按鈕的事件處理程式可以呼叫stopPropagation(),從而避免觸發註冊在document.body上面的事件處理程式。

btn.onclick = function(event){
    alert("clicked");
    event.stopPropagation();
}
document.body.onclick = function(event){
    alert("body clicked");
}

對於這個例子而言,如果不呼叫stopPropagation(),就會在單擊按鈕時出現兩個警告框。使用了之後,由於click事件根本不會被傳播到document.body,因此就不會觸發註冊在document.body上的onclick事件程式。

eventPhase

這個屬性可以用來確定事件當前正位於事件流的哪個階段。

  • 捕獲階段:eventPhase = 1;
  • 處於目標物件上:eventPhase = 2;
  • 冒泡階段:eventPhase = 3;

要注意的是,儘管“處於目標”發生在冒泡階段,但是eventPhase仍等於2.

btn.onclick = function(event){
    alert(event.eventPhase);  //2
}
document.body.addEventListener("click",function(event){
    alert(event.eventPhase);  //1
},true);
document.body.onclick = function(event){
    alert(event.eventPhase);  //3
}

當單擊這個例子中的按鈕時:

  • 首先執行的事件處理程式是在捕獲階段觸發的新增到document.body中的那一個,會彈出一個警告框顯示1
  • 其次會觸發在按鈕上註冊的事件處理程式,此時eventPhase為2
  • 最後在冒泡階段觸發新增到document.body中的那一個,顯示eventPhase為3。

當eventPhase等於2時,this、target、currentTarget始終都是相等的。

只有在事件處理程式執行期間,event物件才會存在;一旦事件處理程式執行完成,event物件就會被銷燬。

IE中的事件物件

與訪問DOM中的event物件不同,要訪問IE中的event物件有幾種不同的方式,取決於指定事件處理程式的方法。

  • 在使用DOM0級方法新增事件處理程式時,event物件作為window隨想的一個屬性存在:
btn.onclick = function(){
    var event = window.event;
    alert(event.type); //"click"
}
  • 如果事件處理程式是通過attachEvent()新增的,那麼就會有一個event物件作為引數被傳入事件處理程式函式中:
btn.attachEvent("onclick",function(event){
    alert(event.type);   //"click"
})

在這種情況下,也可以通過window物件來訪問event物件,就像使用DOM0級方法一樣。不過為了方便起見,同一個物件也會作為引數傳遞。

  • 如果是通過HTML特性指定的事件處理程式,那麼還可以通過一個名為event的變數來訪問event物件(與DOM中的事件模型相同)。
<input type="button" value="click me" onclick="alert(event.type)"/>

IE的event物件同樣也包含與建立它的事件相關的屬性和方法。其中很多屬性和方法都有對應的或者相關的DOM屬性和方法。

srcElement

事件的目標,與DOM中的target屬性相同。
因為事件處理程式的作用域是根據指定它的方式來確定的,所以不能認為this會始終等於事件目標,因此最好還是使用event.srcElement比較保險。

btn.onclick = function(){
    alert(window.event.srcElement == this);  //true
}

btn.attachEvent("onclick",function(event){
    alert(event.srcElement == this);  //false
})

第一個使用DOM0級方法指定的事件處理程式,srcElement屬性等於this。
但在第二個事件處理程式中,由於this指向window,因此這兩者的值不相同。

returnValue

預設值為true,將其設定為false就可以取消事件的預設行為,與DOM中的preventDefault()作用相同。

link.onclick = function(){
    window.event.returnValue = false;
}

cancelBubble

與DOM中的stopPropagation()相同,都是用來停止事件冒泡的。由於IE不支援事件捕獲,因此只能取消事件冒泡。但stopPropagation()可以同時取消捕獲和冒泡。

btn.onclick = function(){
    alert("clicked");
    window.event.cancelBubble = true;
}

document.body.onclick = function(){
    alert("body clicked");
}

通過在onclick事件處理程式中將cancelBubble設定為true,就可以阻止時間通過冒泡而觸發document.body中註冊的事件處理程式。結果就是,單擊按鈕之後,只顯示一個警告框。

跨瀏覽器的事件物件

雖然DOM和IE中的event物件不同,但基於它們之間的相似性依舊可以拿出跨瀏覽器的方案來。
可以對前面的EventUtil物件加以增強:

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

可以像下面這樣使用:

btn.onclick = function(event){
    event = EventUtil.getEvent();
    var target = EventUtil.getTarget(event);
    EventUtil.preventDefault(event);
    EventUtil.stopPropagation(event);
}

別忘了由於IE不支援事件捕獲,因此stopPropagation在跨瀏覽器的情況下,只能用來阻止事件冒泡。