JavaScript事件詳解

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

 

事件流html

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

事件冒泡瀏覽器

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

譬若有以下嵌套的HTML頁面:dom

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

事件捕獲this

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

因爲老版本瀏覽器不支持,所以不多有人使用事件捕獲。

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);
}
相關文章
相關標籤/搜索