JavaScript事件詳解


JavaScript與HTML之間的交互是通過事件來實現的。事件,就是文檔或瀏覽器窗口中發生的一些特定的交互瞬間。可以用偵聽器來預訂事件,以便事件發生的時候執行相應的代碼。

 

事件流

事件流描述了從頁面中接收事件的順序,包括事件冒泡和事件捕獲。

事件冒泡

事件最開始時由最具體的元素(文檔中嵌套層次最深的那個節點)接收,然后逐級向上傳播到較為不具體的節點(文檔)。

譬如有如下嵌套的HTML頁面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Event</title>
</head>
<body>
    <div>
        <p>點擊</p>
    </div>
</body>
</html>

如果點擊p元素,那么click事件首先在p元素上發生,這個元素是我們單擊的元素。然后,click事件沿着DOM樹向上傳播,在每一級節點上都會發生,直到傳播到document對象。傳播順序如下:

p -> div -> body -> html -> document

事件捕獲

事件捕獲的思想是不太具體的節點應該更早接收事件,最具體的節點應該最后接收到事件。事件捕獲的用意在於在事件到達預定目標之前捕獲它。

由於老版本瀏覽器不支持,因此很少有人使用事件捕獲。

DOM事件流

“DOM2級事件”規定的事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段
document -> html -> body -> div -> p-> div -> body -> html -> document

IE8及更早版本不支持DOM事件流

 

事件處理程序

響應事件的函數叫做事件處理程序或事件偵聽器,我們可以通過如下方式為事件指定事件處理程序。

HTML事件處理程序

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

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

這樣指定事件處理程序具有一些獨到之處。首先,這樣會創建一個封裝着元素屬性值的函數。這個函數中有一個局部變量event,也就是事件對象。

<!-- 輸出 'click' -->
<input type="button" value="click me" onclick="alert(event.type)">

通過event變量,可以直接訪問事件對象,不需要自己定義或者從函數的參數列表中讀取。

在這個函數內部,this指向事件的目標元素,例如:

<!-- 輸出 click me-->
<input type="button" value="click me" onclick="alert(this.value)">

關於這個動態創建的函數,另一個有意思的地方是它擴展作用域的方式。在這個函數內部,可以像訪問局部變量一樣訪問document以及該元素本身的成員。這個函數使用with想下面這樣擴展作用域:

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

這樣一來,我們就可以更簡單的訪問自己的屬性,如下和前面的例子效果相同。

<!-- 輸出 click me-->
<input type="button" value="click me" onclick="alert(value)">

如果當前元素是個表單輸入元素,則表用域中還會包含訪問表單元素(父元素)的入口,這個函數就變成了如下所示:

function() {
    with(document) {
        with(this.form) {
            with(this) {
                //元素屬性
            }
        }
    }
}
<!-- username中的值 -->
<form action="bg.php">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="button" value="Click Me" onclick="alert(username.value)">
</form>

使用HTML事件處理程序的缺點

時差問題:用戶可能會在HTML元素一出現在頁面上就觸發相應的事件,但是當時事件處理程序可能不具備執行條件。譬如:

<input type="button" value="click me" onclick="clickFun();">

假設clickFun函數是在頁面最底部定義的,那么在頁面解析該函數之前點擊都會引發錯誤。因此,很多HTML事件處理程序都會被封裝到try-catch之中:

<input type="button" value="click me" onclick="try{clickFun();}catch(ex){}">

瀏覽器兼容問題:這樣擴展事件處理程序的作用域鏈在不同瀏覽器中會導致不同的結果。不同JavaScript引擎遵循的標識符解析規則略有差異,很可能會在訪問非限定對象成員時出錯。

代碼耦合:HTML事件處理程序會導致HTML代碼和JavaScript代碼緊密耦合。如果要更改事件處理成程序需要同時修改HTML代碼和JavaScript代碼。

 

DOM0級事件處理程序

通過JavaScript指定事件處理程序的傳統方式,就是將一個函數賦值給一個事件處理程序屬性。這樣的優勢一是簡單,二是瀏覽器兼容性好。

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

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

var btn = document.getElementById('btn');
btn.onclick = function() {
    alert(this.id); //輸出 'btn'
}

我們可以在事件處理程序中通過this訪問元素的任何屬性和方法。以這種方式添加的事件處理程序會在事件流的冒泡階段被處理。

也可以刪除通過DOM0級方法指定的事件處理程序:

btn.onclick = null;

如果我們使用HTML指定事件處理程序,那么onclick屬性的值就是一個包含着在同名HTML特性中指定的代碼的函數。

<input id="btn" type="button" value="click me" onclick="alert(123);">
<script>
    var btn = document.getElementById('btn');
    //輸出function onclick(event) {  alert(123);} 
    alert(btn.onclick); 
    //單擊按鈕沒反應
    btn.onclick = null;
</script>

 

DOM2級事件處理程序

“DOM2級事件”定義了兩個方法,用於處理指定和刪除事件處理程序的操作:addEventListener和removeEventListener。所有DOM節點中都包含這兩個方法,並且都接收3個參數:要處理的事件名、作為事件處理程序的函數和一個布爾值。如果這個布爾值參數為true,表示在捕獲階段調用事件處理函數;如果是false,表示在冒泡階段調用事件處理函數。

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

與DOM0級方法一樣,添加的事件處理程序也是在其依附的元素的作用域中運行 ,另外,通過這種方式可以添加多個事件處理程序,添加的事件處理程序會按照添加它們的順序出發。

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

問題

我們給一個dom同時綁定兩個點擊事件,一個用捕獲,一個用冒泡,那么事件的執行順序是怎么樣的?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Event</title>
    <style>
        div {
            padding: 30px;
            border: 1px solid #000;
        }
    </style>
</head>
<body>
    <div id="one">
        <div id="two">
            <div id="three">
                <div id="four">Click Me</div>
            </div>
        </div>
    </div>
    <script>
        window.onload = function() {
            one.addEventListener('click',function(){
                alert('one');
            },true);
            two.addEventListener('click',function(){
                alert('two,bubble');
            },false);
            two.addEventListener('click',function(){
                alert('two,capture');
            },true);
            three.addEventListener('click',function(){
                alert('three,capture');
            },true);
            four.addEventListener('click',function(){
                alert('four');
            },true);
        }
    </script>
</body>
</html>

點擊two,執行結果:one   two,bubble   two,capture

點擊three,執行結果:one   two,capture   three,capture   two,bubble

分析:

綁定在被點擊元素的事件是按照代碼順序發生,其他元素通過冒泡或者捕獲“感知”的事件,按照W3C的標准,先發生捕獲事件,后發生冒泡事件。所有事件的順序是:其他元素捕獲階段事件 -> 本元素代碼順序事件 -> 其他元素冒泡階段事件 。

通過addEventListener添加的事件處理程序只能用removeEventListener來移除;移除時傳入的參數與添加處理程序時使用的參數相同,這也就意味着通過addEventListener添加的匿名函數無法移除。

var btn = document.getElementById('btn');

btn.addEventListener('click',function() {
    alert(this.id);
},false);
btn.addEventListener('click',function() {
    alert(this.type);
},false);
//不能移除
btn.removeEventListener('click',function() {
    alert(this.type);
},false)

 大多數情況下,都是將事件處理程序添加到事件流的冒泡階段,這樣可以最大限度地兼容各種瀏覽器。最好只在需要在事件到達目標之前截獲它的時候將事件處理程序添加到捕獲階段。

 IE9+、Firefox、Safari、Chrome、Opera支持DOM2級事件處理程序。

 

IE事件處理程序

IE實現了類似的兩個方法:attachEvent和detachEvent。這兩個方法接收兩個參數:事件處理程序名稱和事件處理程序函數。由於IE8及更早版本只支持事件冒泡,所以通過attachEvent添加的事件處理程序都會被添加到冒泡階段。

var btn = document.getElementById('btn');
btn.attachEvent('onclick',function() {
    alert('clicked');
})

注意第一個參數是onclick而不是click。

使用attachEvent與使用DOM0級方法的主要區別在於事件處理程序的作用域,使用attachEvent時,事件處理程序會在全局作用域中運行,因此this等於window。

var btn = document.getElementById('btn');
btn.attachEvent('onclick',function() {
    alert(this === window);  //true
})

利用attachEvent也可以為一個元素添加多個事件處理程序,但是這些事件處理程序並不是以添加它們的順序執行,而是以相反的順序被執行。

使用attachEvent添加的事件可以通過detachEvent來移除,條件是必須提供相同的參數,所以匿名函數將不能被移除。

支持IE事件處理程序的瀏覽器有IE和Opera,IE11開始將不再支持attachEvent和detachEvent。

跨瀏覽器的事件處理程序

function addEvent(element, type, handler) {
    if (element.addEventListener) {
        //事件類型、需要執行的函數、是否捕捉(false表示冒泡)
        //IE9+支持addEventListener,IE8及以下不支持addEventListener
        element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
        //IE11之后不再支持attachEvent
        //attachEvent添加的時間函數中this指向window
        //IE6-8只支持事件冒泡不支持事件捕獲
        element.attachEvent('on' + type, handler);
    } else {
        element['on' + type] = handler;
    }
}

// 移除事件
function removeEvent(element, type, handler) {
    if (element.removeEventListener) {
        element.removeEventListener(type, handler, false);
    } else if (element.datachEvent) {
        element.detachEvent('on' + type, handler);
    } else {
        element['on' + type] = null;
    }
}

 

事件對象

在觸發DOM上的某個事件時,會產生一個事件對象event,這個對象中包含着所有與事件有關的信息。

DOM中的事件對象

兼容DOM的瀏覽器會將一個event對象傳入到事件處理程序中

var btn = document.getElementById('btn');
btn.onclick = function(event) {
    alert(event.type);
}
btn.addEventListener('click',function(event) {
    alert(event.type);
},false);
<input id="btn" type="button" value="click me" onclick="alert(event.type)">

常用屬性和方法

屬性方法      類型  讀/寫  說明

cancelable     Boolean 只讀  表明是否可以取消事件的默認行為

currentTarget  Element 只讀  其事件處理程序當前正在處理事件的那個元素、

eventPhase    Integer  只讀  調用事件處理程序的階段:1-捕獲階段,2-處於目標,3-冒泡階段

preventDefault   Function  只讀  取消事件默認行為,如果cancelable是true則可以使用這個方法

stopPropagation   Function 只讀  取消事件的進一步捕獲或者冒泡,同時阻止任何事件處理程序被調用(DOM3級事件中新增)

target      Element 只讀  事件的目標

type       String  只讀  被觸發的事件的類型

在事件處理程序內部,this始終等於currentTarget的值,而target則只包含事件的實際目標

如果直接將事件處理程序指定給了目標元素,則this、currentTarget和target包含相同的值。

如果需要通過一個函數處理多個事件時,可以使用type屬性:

var btn = document.getElementById('btn');
var handler = function(event) {
    switch(event.type) {
        case 'click':
            alert('click');
            break;
        case 'mouseover':
            alert('mouseover');
            break;
        case 'mouseout':
            alert('mouseout');
            break;
    }
}
btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

事件對象的eventPhase屬性表示事件當前正位於事件流的哪個階段,需要注意的是盡管“處於目標”發生在冒泡階段,但是eventPhase仍然一支等於2,當eventPhase等於2時,this、target、currentTarget始終是相等的。

注意:只有在事件處理程序執行期間,event對象才會存在,一旦事件處理程序執行完成,event對象就會被銷毀。

 

IE中的事件對象

與訪問DOM中的event對象不同,要訪問IE中的event對象有幾種不同的方式,取決於指定事件處理程序的方法。在使用DOM0級方法添加事件處理程序時,event對象作為window對象的一個屬性存在。

var btn = document.getElementById('btn');
btn.onclick = function() {
    var event = window.event;
    alert(event.type);
}

IE9+中event對象也會作為參數被傳入到事件處理程序中,但是IE9和IE10中參數event和window.event並不是同一個對象,而IE11中參數event和window.event為同一個對象。

var btn = document.getElementById('btn');
btn.onclick = function(event) {
    var event1 = window.event;
    alert(event === event1);  //IE11中為true
}

如果事件處理程序是使用attachEvent添加的,那么就會有一個event對象傳入事件處理函數中,同時我們也可以通過window對象來訪問event對象,但是它們是不相等的。

常用屬性和方法

屬性方法      類型  讀/寫  說明

cancelBubble   Boolean 讀/寫  默認值為false,將其設置為true可以消除事件冒泡

returnValue     Element 讀/寫   默認值為true,將其設置為false可以取消事件的默認行為

srcElement    Element 只讀  事件的目標(相當於DOM中target屬性)

type        String  只讀  被觸發的事件的類型

因為使用attachEvent添加的事件處理程序中this指向window,所以我們通常使用srcElement來代替this。

跨瀏覽器的事件對象

var EventUtil = {
    // 阻止事件 (主要是事件冒泡,因為IE不支持事件捕獲)
    stopPropagation : function(ev) {
        if (ev.stopPropagation) {
            ev.stopPropagation();
        } else {
            ev.cancelBubble = true;
        }
    },
    // 取消事件的默認行為
    preventDefault : function(event) {
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    },
    // 獲取事件目標
    getTarget : function(event) {
        return event.target || event.srcElement;
    },
    // 獲取event對象的引用
    getEvent : function(event) {
        return event ? event : window.event;
    }
}

 

事件代理

因為事件有冒泡機制,所有子節點的事件都會順着父級節點跑回去,所以我們可以通過監聽父級節點來實現監聽子節點的功能,這就是事件代理。

使用事件代理主要有兩個優勢:

  • 減少事件綁定,提升性能。之前你需要綁定一堆子節點,而現在你只需要綁定一個父節點即可。減少了綁定事件監聽函數的數量。
  • 動態變化的 DOM 結構,仍然可以監聽。當一個 DOM 動態創建之后,不會帶有任何事件監聽,除非你重新執行事件監聽函數,而使用事件監聽無須擔憂這個問題。
addEvent(ul2, 'click', handler)
function addEvent(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;
    }
}
function handler(ev) {
    var ev = ev || event;
    var target = ev.target || ev.srcElement;
  //找到a元素
    if (target.nodeName.toLowerCase() == 'a') {
          //a添加的事件
     }
}

jQuery的寫法:

$('#ul1 a').on('click', function(){
    alert('正在監聽');
});
//改為
$('#ul2').on('click', 'a', function(){
    alert('正在監聽');
});

 

總結:

1. addEventListener()和attachEvent()的區別

  • addEventListener(type,handler,capture)有三個參數,其中type是事件名稱,如click,handler是事件處理函數,capture是否使用捕獲,是一個布爾值,一般為false,這是默認值,所以第三個參數可以不寫。attachEvent('on'+type,handler)有兩個參數,其中type是事件名稱,如click,第一個參數必須是onxxxx,handler是事件處理函數,IE6 IE7 IE8不支持事件捕獲,只支持事件冒泡。
  • addEventListener綁定的事件是先綁定先執行,attachEvent綁定的事件是先綁定后執行
  • 使用了attachEvent或detachEvent后事件處事函數里面的this指向window對象,而不是事件對象元素

2. 解決attchEvent事件處理函數中 this指向window的方法

1) 使用事件處理函數.apply(事件對象,arguments)
這種方式的缺點是綁定的事件無法取消綁定,原因上面已經說了,匿名函數和匿名函數之間是互不相等的。

var object=document.getElementById('xc');
function handler(){
    alert(this.innerHTML);
}
object.attachEvent('onclick',function(){
    handler.call(object,arguments);
});

2) 使用事件源代替this關鍵字
以下代碼僅適用於IE6 IE7 IE8,這種方式完全忽略this關鍵字,但寫起來稍顯麻煩。

function handler(e){
    e = e||window.event;
    var _this = e.srcElement||e.target;
    alert(_this.innerHTML);
}
var object = document.getElementById('xc');
object.attachEvent('onclick',handler);
3) 寫一個函數完全代替attachEvent/detachEvent,並且支持所有主流瀏覽器、解決IE6 IE7 IE8事件綁定導致的先綁定后執行問題。
注意,本函數是全局函數,而不是DOM對象的成員方法。 
/*
 * 添加事件處理程序
 * @param object object 要添加事件處理程序的元素
 * @param string type 事件名稱,如click
 * @param function handler 事件處理程序,可以直接以匿名函數的形式給定,或者給一個已經定義的函數名。
 * @param boolean remove 是否是移除的事件,本參數是為簡化下面的removeEvent函數而寫的,對添加事件處理程序不起任何作用
*/
function addEvent(object,type,handler,remove){
    if(typeof object != 'object' || typeof handler != 'function') return;
    try{
        object[remove ? 'removeEventListener' : 'addEventListener'](type,handler,false);
    } catch( e ){
        var i, l, xc = '_' + type;
        object[xc] = object[xc] || [];
        if(remove){
            l = object[xc].length;
            for(i = 0;i < l;i++){
                if(object[xc][i].toString() === handler.toString()){
                    object[xc].splice(i,1);
                }
            }
        } else{
            l = object[xc].length;
            var exists = false;
            for(i = 0;i < l;i++){                                                
                if(object[xc][i].toString() === handler.toString()) {
                    exists = true;
                }
            }
            if(!exists) object[xc].push(handler);
        }
        object['on' + type] = function(){
            l = object[xc].length;
            for(i = 0;i < l;i++){
                object[xc][i].apply(object,arguments);
            }
        }
    }
}
/*
* 移除事件處理程序
*/
function removeEvent(object,type,handler){
    addEvent(object,type,handler,true);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM