JavaScript 之事件處理詳解

一.事件傳播機制


客戶端JavaScript程序(就是瀏覽器啦)採用了異步事件驅動編程模型。當文檔、瀏覽器、元素或與之相關的對象發生某些有趣的事情時,Web瀏覽器就會產生事件(event)。如果JavaScript應用程序關注特定類型的事件,那麼它可以註冊當這類事件發生時要調用的一個或多個函數。當然了,這種風格並非Web編程獨有,所有使用圖形用戶界面的應用程序都採用了它。


既然要詳解事件處理,那我們先從幾個基礎概念說起吧:


①事件類型(event type):是一個用來說明發生什麼類型事件的字符串。例如,“mousemove”表示用戶移動鼠標,“keydown”表示鍵盤上某個鍵被按下。事件類型只是一個字符串,有時候又稱之爲事件名字(event name);


②事件目標(event target):是發生事件或與之相關的對象。Window、Document和Element對象是最常見的事件目標。當然,AJAX中的XMLHttpRequest對象也是一個事件目標;


③事件處理程序(event handler):是處理或響應事件的函數,它也叫事件監聽程序(event listener)。應用程序通過指明事件類型和事件目標,在Web瀏覽器中註冊它們的事件處理函數。


④事件對象(event object):是與特定事件相關且包含有關該事件詳細信息的對象。事件對象作爲參數傳遞給事件處理函數(但是在IE8以及其之前版本中,全局變量event纔是事件對象)。事件對象都有用來指定事件類型(event type)的type屬性和指定事件目標(event target)的target屬性(但是在IE8以及其之前版本中,用的是srcElement而非target)。當然,不同類型的事件還會爲其相關事件對象定義一些其他的獨有屬性。例如,鼠標事件的相關對象會包含鼠標指針的座標,而鍵盤事件的相關對象會包含按下的鍵和輔助鍵的詳細信息。


以上說完了四個基本概念。那麼問題來了——如果在一個web頁面上用鼠標點擊一個元素a的某一子元素b時,應該先執行子元素b註冊的事件處理程序還是先執行元素a註冊的事件處理程序呢(假設元素a和它的子元素b都有註冊事件處理程序)?身爲讀者的你是否想過這個問題呢?


這個問題就涉及到瀏覽器中的事件傳播(event propagation)機制。相信大家都聽說過事件冒泡(event bubble)和事件捕獲(event capturing)吧!沒錯,它們就是瀏覽器中的事件傳播機制。無圖無真相,沒有配圖?那怎麼闊以:



看了圖之後相信你已經大概理解了瀏覽器中的事件傳播機制了:當一個事件發生時,它會先從瀏覽器頂級對象Window一路向下傳遞,一直傳遞到觸發這個事件的那個元素,這也就是事件捕獲過程。然而,一切並沒有結束,事件又從這個元素一路向上傳遞到Window對象,這也就是事件冒泡過程(但是在IE8以及其之前版本中,事件模型並未定義捕獲過程,只有冒泡過程)。


所以,關於上面的問題,還得看元素a註冊的事件處理程序是在捕獲過程還是在冒泡過程了。那麼到底什麼是在捕獲過程註冊事件處理程序,在冒泡過程註冊事件處理程序又是怎麼做的呢?這就得好好說說幾種註冊事件處理程序的方式了:


1. 設置HTML標籤屬性爲事件處理程序


文檔元素的事件處理程序屬性,其名字由“on”後面跟着事件名組成,例如:onclick、onmouseover。當然了,這種形式只能爲DOM元素註冊事件處理程序。實例:


<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<title>test</title>

<style type="text/css">

#div1{width: 300px; height: 300px; background: red; overflow:hidden;}

#div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;}

#div3{margin:50px auto; width: 100px; height: 100px; background: blue;}

</style>

</head>

<body>

<div id="div1" onClick="console.log('div1');">div1

<div id="div2" oNClick="console.log('div2');">div2

<div id="div3" onclick="console.log('div3');" onclick="console.log('div3333');">div3

</div>

</div>

</div>

<script type="text/javascript">

</script>

</body>

</html>


結果(鼠標點擊div3區域後):



從結果中可以看出:


①因爲HTML裏面不區分大小寫,所以這裏事件處理程序屬性名大寫、小寫、大小混寫均可,屬性值就是相應事件處理程序的JavaScript代碼;


②若給同一元素寫多個onclick事件處理屬性,瀏覽器只執行第一個onclick裏面的代碼,後面的會被忽略;


③這種形式是在事件冒泡過程中註冊事件處理程序的;


2.設置JavaScript對象屬性爲事件處理程序


可以通過設置某一事件目標的事件處理程序屬性來爲其註冊相應的事件處理程序。事件處理程序屬性名字由“on”後面跟着事件名組成,例如:onclick、onmouseover。實例:


<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<title>test</title>

<style type="text/css">

#div1{width: 300px; height: 300px; background: red; overflow:hidden;}

#div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;}

#div3{margin:50px auto; width: 100px; height: 100px; background: blue;}

</style>

</head>

<body>

<div id="div1">div1

<div id="div2">div2

<div id="div3">div3

</div>

</div>

</div>

<script type="text/javascript">

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

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

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

  div1.onclick = function(){

    console.log('div1');

  };

  div2.onclick = function(){

    console.log('div2');

  };

  div3.onclick = function(){

    console.log('div3');

  };

  div1.onclick = function(){

    console.log('div11111');

  };

  div1.onClick = function(){

    console.log('DIV11111');

  };

</script>

</body>

</html>


結果(鼠標點擊div3區域後):



從結果中可以看出:


①因爲JavaScript是嚴格區分大小寫的,所以,這種形式下屬性名只能按規定小寫;


②若給同一元素對象寫多個onclick事件處理屬性,後面寫的會覆蓋前面的(ps:這就是在修改一個對象屬性的值,屬性的值是唯一確定的);


③這種形式也是在事件冒泡過程中註冊事件處理程序的;


3.addEventListener()


前兩種方式出現在Web初期,衆多瀏覽器都有實現。而addEventListener()方法是標準事件模型中定義的。任何能成爲事件目標的對象——這些對象包括Window對象、Document對象和所有文檔元素等——都定義了一個名叫addEventListener()的方法,使用這個方法可以爲事件目標註冊事件處理程序。addEventListener()接受三個參數:第一個參數是要註冊處理程序的事件類型,其值是字符串,但並不包括前綴“on”;第二個參數是指當指定類型的事件發生時應該調用的函數;第三個參數是布爾值,其可以忽略(某些舊的瀏覽器上不能忽略這個參數),默認值爲false。這種情況是在事件冒泡過程中註冊事件處理程序。當其爲true時,就是在事件捕獲過程中註冊事件處理程序。實例:


<!DOCTYPE HTML>

<html>

<head>

  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

  <title>test</title>

  <style type="text/css">

    #div1{width: 300px; height: 300px; background: red; overflow:hidden;}

    #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;}

    #div3{margin:50px auto; width: 100px; height: 100px; background: blue;}

  </style>

</head>

<body>

  <div id="div1">div1

    <div id="div2">div2

      <div id="div3">div3

      </div>

    </div>

  </div>

<script type="text/javascript">

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

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

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

  div1.addEventListener('click', function(){ console.log('div1-bubble'); }, false);

  div2.addEventListener('click', function(){ console.log('div2-bubble'); }, false);

  div3.addEventListener('click', function(){ console.log('div3-bubble'); }, false);

  div3.addEventListener('click', function(){ console.log('div3-bubble222'); }, false);

  div1.addEventListener('click', function(){ console.log('div1-capturing'); }, true);

  div2.addEventListener('click', function(){ console.log('div2-capturing'); }, true);

  div3.addEventListener('click', function(){ console.log('div3-capturing'); }, true);

</script>

</body>

</html>


結果(鼠標點擊div3區域後):



從結果中可以看出:


①addEventListener()第三個參數的作用正如上面所說;


②通過addEventListener()方法給同一對象註冊多個同類型的事件,並不會發生忽略或覆蓋,而是會按順序依次執行;


相對addEventListener()的是removeEventListener()方法,它同樣有三個參數,前兩個參數自然跟addEventListener()的意義一樣,而第三個參數也只需跟相應的addEventListener()的第三個參數保持一致即可,同樣可以省略,默認值爲false。它表示從對象中刪除某個事件處理函數。實例:


div1.addEventListener('click', div1BubbleFun, false);

div1.removeEventListener('click', div1BubbleFun, false);

function div1BubbleFun(){

console.log('div1-bubble');

}


4.attachEvent()


但是,IE8以及其之前版本的瀏覽器並不支持addEventListener()和removeEventListener()。相應的,IE定義了類似的方法attachEvent()和detachEvent()。因爲IE8以及其之前版本瀏覽器也不支持事件捕獲,所以attachEvent()並不能註冊捕獲過程中的事件處理函數,因此attachEvent()和detachEvent()要求只有兩個參數:事件類型和事件處理函數。而且,它們的第一個參數使用了帶“on”前綴的事件處理程序屬性名。實例:


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

div1.attachEvent('onclick', div1BubbleFun);

function div1BubbleFun(){

  console.log('div1-bubble');

}


相應的,從對象上刪除事件處理程序函數使用detachEvent()。例如:


div1.detachEvent('onclick', div1BubbleFun);


到此爲止,我們已經說了瀏覽器中事件傳播機制以及各種註冊事件處理程序的方法。下面我們就再說說事件處理程序調用時的一些問題吧!


二.事件處理程序的調用


1.事件處理程序的參數:正如前面所說,通常事件對象作爲參數傳遞給事件處理函數,但IE8以及其之前版本的瀏覽器中全局變量event纔是事件對象。所以,我們在寫相關代碼時應該注意兼容性問題。實例(給頁面上id爲div1的元素添加點擊事件,當點擊該元素時在控制檯輸出事件類型和被點擊元素本身):


<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<title>test</title>

<style type="text/css">

#div1{width: 300px; height: 300px; background: red; overflow: hidden;}

</style>

</head>

<body>

<div id="div1">div1</div>

<script type="text/javascript">

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

if(div1.addEventListener){

div1.addEventListener('click', div1Fun, false);

}else if(div1.attachEvent){

div1.attachEvent('onclick', div1Fun);

}

function div1Fun(event){

event = event || window.event;

var target = event.target || event.srcElement;

console.log(event.type);

console.log(target);

}

</script>

</body>

</html>


2.事件處理程序的運行環境:關於事件處理程序的運行環境,也就是在事件處理程序中調用上下文(this值)的指向問題,可以看下面四個實例。


實例一:


<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<title>test</title>

<style type="text/css">

#div1{width: 300px; height: 300px; background: red; overflow: hidden;}

</style>

</head>

<body>

<div id="div1" onclick="console.log('html:'); console.log(this);">div1</div>

<script type="text/javascript">

</script>

</body>

</html>


結果一:



從結果可以看出:


①第一種方法事件處理程序中this指向這個元素本身;


實例二:


<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<title>test</title>

<style type="text/css">

#div1{width: 300px; height: 300px; background: red; overflow: hidden;}

</style>

</head>

<body>

<div id="div1" onclick="console.log('html:'); console.log(this);">div1</div>

<script type="text/javascript">

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

div1.onclick = function(){

console.log('div1.onclick:');

console.log(this);

};

</script>

</body>

</html>


結果二:



從結果可以看出:


①第二種方法事件處理程序中this也指向這個元素本身;


②存在第二種方法時,它會覆蓋第一種方法註冊的事件處理程序;


實例三:


<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<title>test</title>

<style type="text/css">

#div1{width: 300px; height: 300px; background: red; overflow: hidden;}

</style>

</head>

<body>

<div id="div1" onclick="console.log('html:'); console.log(this);">div1</div>

<script type="text/javascript">

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

div1.onclick = function(){

console.log('div1.onclick:');

console.log(this);

};

div1.addEventListener('click', function(){

console.log('div1.addEventListener:');

console.log(this);

}, false);

</script>

</body>

</html>


結果三:



從結果可以看出:


①第三種方法事件處理程序中this也指向這個元素本身;


②第三種方法並不會覆蓋第一種或第二種方法註冊的事件處理程序;


實例四:


<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<title>test</title>

<style type="text/css">

#div1{width: 300px; height: 300px; background: red; overflow: hidden;}

</style>

</head>

<body>

<div id="div1" onclick="console.log('html:'); console.log(this);">div1</div>

<script type="text/javascript">

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

div1.onclick = function(){

console.log('div1.onclick:');

console.log(this);

};

div1.attachEvent('onclick', function(){

console.log('div1.attachEvent:');

console.log(this === window);

});

</script>

</body>

</html>


結果四:



從結果可以看出:


①第四種方法事件處理程序中this指向全局對象Window;


②第四種方法也不會覆蓋第一種或第二種方法註冊的事件處理程序;


3.事件處理程序的調用順序:多個事件處理程序調用規則如下:


①通過HTML屬性註冊的處理程序和通過設置對象屬性的處理程序一直優先調用;


②使用addEventListener()註冊的處理程序按照它們的註冊順序依次調用;


③使用attachEvent()註冊的處理程序可能按照任何順序調用,所以代碼不應該依賴於調用順序;


4.事件取消:


①取消事件的瀏覽器默認操作(比如點擊超鏈接元素會自動發生頁面跳轉的默認操作):如果使用前兩種方法註冊事件處理程序,可以在處理程序中添加返回值false來取消事件的瀏覽器默認操作。在支持addEventListener()的瀏覽器中,也可以通過調用事件對象的preventDefault()方法取消事件的默認操作。至於IE8及其之前的瀏覽器可以通過設置事件對象的returnValue屬性爲false來取消事件的默認操作。參考代碼:


function cancelHandler(event){

var event = event || window.event;

if(event.preventDefault){

event.preventDefault();

}

if(event.returnValue){

event.returnValue = false;

}

return false;

}


②取消事件傳播:在支持addEventListener()的瀏覽器中,可以調用事件對象的一個stopPropagation()方法阻止事件的繼續傳播,它能工作在事件傳播期間的任何階段(捕獲期階段、事件目標本身、冒泡階段);但是在IE8以及其之前版本的瀏覽器中並不支持stopPropagation()方法,而且這些瀏覽器也不支持事件傳播的捕獲階段,相應的,IE事件對象有一個cancelBubble屬性,設置這個屬性爲true能阻止事件進一步傳播(即阻止其冒泡)。參考代碼(阻止發生在div3區域的點擊事件冒泡到div2和div1):


<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

<title>test</title>

<style type="text/css">

#div1{width: 300px; height: 300px; background: red; overflow:hidden;}

#div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;}

#div3{margin:50px auto; width: 100px; height: 100px; background: blue;}

</style>

</head>

<body>

<div id="div1">div1

<div id="div2">div2

<div id="div3">div3

</div>

</div>

</div>

<script type="text/javascript">

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

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

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

   div1.onclick = function(){

console.log('div1');

   };

   div2.onclick = function(){

console.log('div2');

   };

   div3.onclick = function(event){

stopEventPropagation(event);

console.log('div3');

   };

function stopEventPropagation(event){

var event = event || window.event;

if(event.stopPropagation){

event.stopPropagation();

}else{

event.cancelBubble = true;

}

}

</script>

</body>

</html>


當然,關於事件冒泡還是有可利用之處的,這也就是我們常說的事件代理或者事件委託。


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