邊做邊學jQuery系列07 - jQuery事件處理

課程錄製時間: 2009年
jQuery事件處理

【事件處理】

網頁如果對使用者點選拖移動作都沒有任何反應,與報紙或書本一樣純供閱讀,瀏覽使用的樂趣肯定要大打折扣。要把握與使用者互動的時機,就非得透過事件機制不可。雖然我們到目前才開始談事件,但先前的範例中,.click(function() { ... })的寫法早已不可避免地多次扮演過關鍵角色。

大部分開發人員初學Javascript的事件寫法,大多長得像這個樣子:

<span id="mySpan" onclick="spnClick();">Click</span>
<script type="text/javascript">
function spnClick() { 
	alert("Clicked!"); 
}
</script>

或者我們也可以寫成document.getElementById("mySpan").onclick = spnClick; 不過這種寫法很粗魯地將整個事件佔為己有,不管是否原先上面是已經掛載了其他事件函數,並不嚴謹。IE提供了attachEvent()、Firefox提供了attchEventListener() 可以在一個元素事件上重覆掛載多個事件函數。看到不同瀏覽器要用不同寫法先別煩惱,jQuery當然已經替我們解決了跨瀏覽器問題,在jQuery中,我們要掛上事件函數,可以透過統一的寫法:

$(...).bind(type, data, function() { ... });

依元素的種類,type用來指名要掛載的事件名稱字串(可以是blur, focus, load, resize, scroll, unload, click, dblclick, mousedown, mouseup, mousemove, mouseover, mouseout, mouseenter, mouseleave, change, select, submit, keydown, keypress, keyup, error其中之一,多個事件名稱以空白相接則可將同一函數設在多個事件上)

事件 說明
blur 元素失去焦點時
focus 元素取得焦點時
load 元素完成載入時
resize 元素大小改變時
scroll 當捲軸捲動時
unload 使用者關閉離開網頁時
click 滑鼠點擊元素時
dblclick 滑鼠連點二下元素時
mousedown 按下滑鼠按鍵時
mouseup 放開滑鼠按鍵時
mousemove 介於mouseover跟mouseout間的滑鼠移動
mouseover 滑鼠進入元素範圍時(滑鼠離開子元素但仍在其元素範圍內時也會觸發mouseover)
mouseout 滑鼠離開元素範圍時(在元素範圍內,滑鼠進入子元素範圍時也會觸發mouseout)
mouseenter 滑鼠進入元素範圍時
mouseleave 滑鼠離開元素範圍時
change 欄位內容改變時
select 使用者選取元素中的文字時
submit HTML Form送出時
keydown 按下鍵盤按鍵時
keypress 按下按鍵後放開按鍵前觸發
keyup 放開鍵盤按鍵時

回到$(...).bind(type, data, function() { ... });上,data函數主要是用來傳遞額外的參數,這在多個事件共用函數時特別有用,例如:

function sharedEvent(e) {
   alert("Data=" + e.data);
}
$("span").bind("click", "SpanClick", sharedEvent);
$("div").bind("click", "DivClick", sharedEvent);

不過,一向以簡潔見長的jQuery,在事件宣告上有更簡便的寫法,例如 $(...).bind("click", function() { ... })可以直接寫成$(...).click(function() { ... });

要卸除事件函數則可透過unbind([type], [fn]),例如: unbind()可卸除所有事件函數、unbind("click")可以卸除所有的click事件函數、unbind("click", funcA)則可只卸除funcA。理論上,元素由DOM移除時應該要將其上所掛載的事件函數全部卸除以避免惱人的記憶體洩漏(Memory Leak)問題,所幸jQuery在這方面下了不少功夫,幾乎都能自動在unload時加上處理,除非有特殊的目的要卸除事件才要特別unbind(),倒不需額外傷腦筋。

事件允許多重宣告,並會依宣告先後依序觸發,如果我們要由程式觸發事件,做法也很簡單,$(...).trigger(event, data);即可(event可以是事件名稱字串、jQuery.Event物件、或是自訂物件,詳細說明可見API參考文件),或是比照宣告時,簡寫成$(...).click();,差別只在於中間不傳入事件函數,例如:

$("input:button").click(function() { alert("A"); });
$("input:button").click(function() { alert("B"); });
$("input:button").click();

【存取事件資訊】

在事件函數中,可以透過this存取觸發事件的元素物件,如果要取得滑鼠座標、按鍵內容等資訊,則在函數中接收event參數取得事件資訊物件。例如: mousedown事件時,可以透過pageX/pageY取得相對於網頁的滑鼠座標,screenX/screenY取得相對於螢幕的滑鼠座標、keydown事件時,則可由which取得按鍵碼。以下為範例:

$("#Inner").mousedown(function(evt) {
	alert("P:" + evt.pageX + "," + evt.pageY);
});
$("input").keydown(function(evt) {
	alert(evt.which);
});

除了元素的既有事件外,我們也可以自訂事件,方法是先bind到自訂的事件名稱上,之後再以trigger觸發,例如:

$("div").bind("beat", function(event, attacker, weapon) { 
	alert("被" + attacker + "用" + weapon + "痛扁一頓!"); });
//不傳入event
$("div").trigger("beat", [ "Jeffrey", "狼牙棒" ]);
//傳入event
var e = jQuery.Event("beat"); //jQuery 1.3+支援
$("div").trigger(e, [ "Darkthread", "愛的小手" ]);

【事件物件】

關於事件物件(jQuery.Event),在此針對幾個常用屬性、方法補充說明: (事件物件自jQuery 1.3起做了一番改寫,在此以jQuery 1.3版本為準)

  1. 事件函數中除了用this可以存取被觸發的元素外,還有幾個相關屬性: target適用於由Bubble引發事件時,指向真正觸發事件的元素,用一個例子較易說明: 有一個<div id="d"><span id="s">TEXT</span></div>,當span被click時,會進一步也觸發div的click事件,因此若寫成
    		
    $("div").click(function(e) { 
    	alert($(this).attr("id")); 
    	alert($(e.target).attr("id")); 
    });
    
    就會分別得到"d"與"s"。屬性currentTarget原則上就等同於this。relatedTarget用於父子元素間mouseover與mouseout事件切換時偵察之用,稍候會再另做討論。
  2. IE原本不支援pageX/pageY,jQuery補上了。
  3. result可用於同一事件的多個事件函數間傳遞資料,例如:
    $("div").click(function() { return "First Event Handler's Result"; });
    $("div").click(function(e) { 
    	alert(e.result);
    }).click();
    
  4. 某些事件函數中,可以控制阻止作業繼續進行,例如: <a>的onclick事件可以阻止連向該連結、<form>的onsubmit可以拒絕將表單送出。在做法上各家瀏覽器有所不同,jQuery則統一可透過event.preventDefault()達成取消的目的。例如: $("a").click(function(e) { e.preventDefault(); });將會阻止瀏覽器轉接超連結。
  5. 借用第一點中提到的Bubble原理,span被點選時,除了span的click事件被觸發,span的父元素div也會被觸發click事件,就像氣泡一樣一路浮上去。如果我們希望span click事件觸發後就結束,不要再觸發其父元素的click事件,可以使用event.stopPropagation()達到目標。如下例,將e.stopPropagation();前方的註解移除,就只會看到alert SPAN:
    $("div").click(function(e) { alert("DIV"); });
    $("span").click(function(e) { 
    	alert("SPAN"); 
    	//e.stopPropagation(); 
    });
    
  6. event.stopImmediatePropagation() 類似event.stopPropagation()的概念,但停止觸發的對象除了父元素外,還包含同一元素針對該事件多重宣告的其他函數

【好用的mouseenter、mouseleave】

先前在談relatedTarget時提到它跟mouseover/mouseout有關,如果不了解mouseover, mouseout有什麼缺點,就不會知道mouseenter/mouseleave好在哪裡。

基本的概念是,當滑鼠滑入元素範圍時,會先觸發mouseover、接著在元素上游移時會觸發mousemove、等移出元素時會觸發mouseout,邏輯看來很清楚,應該沒有什麼問題。當元素中包有子元素時,這一切就變了調,滑鼠在父元素中移動時,當其進入子元素的範圍,會觸發父元素的mouseout,才觸發子元素的mouseover。換句話說,雖然子元素也被包含在父元素中,但進到子元素領空時,瀏覽器的認知是這也算離開父元素的範圍(雖然我們較傾向認定子元素也是父元素的一部分),這在處理邏輯上會造成一些困擾。光用描述的有點抽象,因此我們用一個實例來展示它有多機車:

<html>   
<head>   
<style type='text/css'>   
div { border: solid 1px black; }  
#Outer { width: 100px; height: 100px; }  
#Inner { width: 50px; height: 50px; margin: 25px; }   
</style>   
<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.js'></script>   
<script type='text/javascript'>   
$(function() {   
  
$("div:not(#dvDisp)").bind("mouseover", "Over", mouseEvent)   
.bind("mouseout", "Out", mouseEvent);   
function mouseEvent(evt) {   
  $("#dvDisp").append("<span>" + $(this).attr("title") + ":" + evt.data + "-></span>");   
}   
});   
</script>   
</head><body>   
  
<div id="Outer" title="外">   
<div id="Inner" title="內"></div>   
</div><hr />   
<div id="dvDisp"></div>   
  
</body>   
</html> 

當我們用滑鼠從左到右一口氣穿透Outer與Inner兩個div時(如下圖中的箭頭方向),觸發事件的順序是: 外:Over->外:Out->內:Over->外:Over->內:Out->外:Out->外:Over->外:Out,夠複雜吧?

大部分時候,我們期望看到的應是"外:Over->內:Over->內:Out->外:Out"才對。IE很貼心地多提供了mouseenter, mouseleave實踐我們理想中的事件順序,但非IE瀏覽器怎麼辦? 這回jQuery又施展魔法,讓mouseenter/mouseleave變成跨瀏覽器的事件。程式小改:

$("div:not(#dvDisp)").bind("mouseenter", "Enter", mouseEvent)
.bind("mouseleave", "Leave", mouseEvent);
function mouseEvent(evt) {
  $("#dvDisp").append("<span>" + $(this).attr("title") + ":" + evt.data + "-></span>");
}

結果變成令人感動的"外:Enter->內:Enter->內:Leave->外:Leave",而且在FireFox中也可以通。

再一次,jQuery讓跨瀏覽器的艱鉅工程變簡單了。

【強化型事件裝卸功能】

除了標準的bind之外,有時我們會需要一次性的事件處理邏輯,傳統做法是在事件函數中將自己unbind。jQuery則有另一個one()可以省去我們自己unbind的功夫,在事件觸發後就會自動卸除。

另外,針對mouseover、mouseout時要分別呼叫對應的事件函數,jQuery提供了hover(overFunc, outFunc)的一次宣告法,可以再省點工。而如果是第一次點選執行fn1,第二次點選fn2,第三次fn3的情境,jQuery則有toggle(fn1, fn2, fn3, ...)的簡便宣告方式。

另外,使用$("...").bind(...)的寫法,只會作用在bind(...)執行當下已存在的元素上,若事後又有符合該Selector的元素新增,並不會自動掛上該事件,需要額外的程式邏輯再處理一次。

jQuery 1.3起有個live()與die(),跟bind()/unbind()的目標相同,但是可以做到未來符合Selector的元素一旦出現,就會自動掛上事件的功能。請看以下示範:

<html>
<head>
<style type='text/css'>
div { background-color: #dddddd; }
</style>
<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.js'></script>
<script type='text/javascript'>
$(function() {
$("#xxx").bind("click", function() { alert("YA!"); });
$("input:button").click(function() {
	$("body").append("<div id=\"xxx\">XXX</span>");
});
});
</script>
</head><body>
<input type="button" value="TEST" />
</body>
</html>

這個範例是不會成功的,因為在第9列$("#xxx").bind(...)執行時,<div id="xxx">還不存在,等按鈕新增後,並未再bind一次,因此上面沒有任何事件。但如果我們把第9列的bind改為live(記得要用jQuery 1.3+),就可以看到神奇的事發生,先對不存在的元素宣告事件,待元素一被建立,事件立即生效。

【範例檔案下載】

歡迎推文分享:



 
RSS
【工商服務】
OrcsWeb: Windows Server Hosting
twMVC

關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。