聲明:寫博客,是對自身知識的一種總結,也是一種分享,但由於作者本人水平有限,難免會有一些地方說得不對,還請大家友善 指出,或致電:[email protected]
關注:國內開源jQuery-UI組件庫:Operamasks-UI
jQuery版本:v1.7.1
一. 有感而發
處理過前端腳本事件的朋友都清楚,各瀏覽器在處理DOM上的事件的不一致性讓人煩不勝煩。而爲了提供一致的訪問接口,jQuery作者提供了一套犀利的解決方案,這種思想是值得我們借鑑和學習的。
二.傳統事件處理蔽端
多說無益,我們直接從以下例子來看看瀏覽器間在事件處理方面的一些不一致性。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="jquery-1.7.1.js"></script>
<script>
function f1(event){
//IE上事件對象從window.event獲取,其它瀏覽器一般會以參數傳進來
event = event || window.event;
alert(event.pageX);
}
function bind(){
var btn = document.getElementById("btn");
if(document.attachEvent){
btn.attachEvent("onclick" , f1);
btn.attachEvent("onclick" , f1);
}else if(document.addEventListener){
btn.addEventListener("click" , f1 , false);
btn.addEventListener("click" , f1 , false);
}
}
/**測試結果:
**在火狐上,彈出兩個窗口,分別打印出 "original click." "50"
**在IE9上,彈出兩個窗口,分別打印出 "original click." "undefined"
**在IE8上,彈出三個窗口,分別打印出 "original click." "undefined" "undefined"(兩個undefined,你沒有看錯)
*/
</script>
</head>
<body οnlοad="bind()">
<input id="btn" type="button" value="click me" οnclick="alert('original click.');"/>
</body>
</html>
就上面這個非常簡單的例子,我們就可以看出幾個瀏覽器間事件處理會出現不一致的地方
1. 事件的綁定方式不同
2. 獲取event對象的方式不同
3. event對象中的數據不完全相同
4. 對重複添加同一處理函數的處理方式不同,有人疊加有人忽略
還有一些例子沒有體現出來,比如不可以添加兩個處理事件,後添加的會覆蓋前面的;阻止事件冒泡的方式和阻止其默認行爲;還有什麼,等你來挖掘。
既然有如此多的不一致性,jQuery作者當然想辦法要進行屏蔽了,而大師的處理方式將是本文章要探討的內容。
三.jQuery的事件處理方式
3.1 自己實現事件處理
事件綁定方式不同,這個解決不難,只要不同瀏覽器採用不同綁定方式,像第二部分的例子一樣就行了。但如何處理類似不可以添加多個處理函數這種侷限?我們自己來試着解決一下:
eventHandle<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="jquery-1.7.1.js"></script>
<script>
window.guid = 0;//標記各個不同的DOM節點
var cache = {};//緩存對象,用於存儲事件處理器
function bind(elem , type , handler){
//給dom節點添加一個唯一屬性,用於標識該 dom節點
if(!elem["guid"]){
elem["guid"] = ++window.guid;
}
var events = cache[elem["guid"]];
if(!events){
//初始化dom節點的緩存數據對象
cache[elem["guid"]] = events = {};
events[type] = events[type] || [];
//同一個dom節點只註冊一次事件,那就是eventHandle,然後eventHandle再調用dispatch進行分發
if(document.attachEvent){
elem.attachEvent("on"+type , eventHandle);
}else if(document.addEventListener){
elem.addEventListener(type , eventHandle , false);
}
}
events[type].push(handler);
}
function dispatch(elem , event){
var events = window.cache[elem["guid"]],
ret;
if(!events){
return ;
}
var handlers = events[event.type];
for(var i=0,len=handlers.length; i<len; i++){
//event作爲參數傳遞過去
ret = handlers[i].call(elem , event);
}
if(ret != undefined){
if ( ret === false ) {
event.preventDefault();
event.stopPropagation();
event.cancleBubble = true;
event.returnValue = false;
}
}
}
function eventHandle(event){
event = event || window.event;
dispatch(this , event);
}
function test(){
var btn = document.getElementById("btn");
bind(btn , "click" , function(){alert("click1");});
//當下邊改爲bind(btn , "click" , function(){alert("click2");return false});時,
//點擊按鈕不會顯示 "wrapper",因爲取消冒泡了
bind(btn , "click" , function(){alert("click2");});
var wrapper = document.getElementById("wrapper");
bind(wrapper , "click" , function(){alert("wrapper");});
}
//點擊按鈕後,彈出窗口依次顯示 : click1 click2 wrapper
</script>
</head>
<body οnlοad="test()">
<div id="wrapper">
<input id="btn" type="button" value="click me"/>
</div>
</body>
</html>
以上代碼雖然不是很多,但卻是jQuery處理事件的一個小縮影。我們統一給dom節點註冊了一個事件eventHandle,也就是說不管該dom觸發什麼類型的事件,一定會執行eventHandle,然後在eventHandle中再調用dispatch進行分發,也就是根據事件類型在緩存中查找相應的處理器。這樣可以得到很多好處,最明顯的就是可以添加多個處理器了,以後用戶再也不用怕兼容性問題了,我們全部都隱藏在了內部實現中。當綁定完後,我們可以看下緩存是怎樣的。
如果看懂了以上這個 event 的示例,那麼jQuery關於事件的處理最核心的東西你已經知道了,接下來我們來看下jQuery的做法。
3.2 jQuery的事件處理
先來看個流程圖:
上圖是一個處理流程圖,jQuery分幾步來處理:
1. 給每個dom節點註冊一個唯一的處理器(eventHandle)
2. 不管該dom觸發了什麼事件,都會執行eventHandle, 然後eventHandle調用dispatch進行事件的真正處理
3. dispatch進行事件的修正,用jQuery自定義的事件類型來包裝原生的event對象,這樣提供統一的訪問接口,如event.target在任何瀏覽器下表示觸發事件的源dom節點。
4. 在jQuery公用緩存中獲取該dom節點的處理器數據,由於委託機制的存在(稍後再講),所以先獲取此節點的委託處理器列表。
5. 把此節點的委託處理器列表與本節點本身的處理器列表進行合併,形成最終的處理器列表。
6. 執行最終的處理器列表,並即時處理取消冒泡和阻止瀏覽器默認行爲。
看到這裏,發現大體思想還是比較簡單的吧,那我們再來深入一些細節,首先看看當綁定事件後緩存的存儲情況怎樣?
Html代碼
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type"content="text/html; charset=UTF-8">
<script type="text/javascript"src="jquery-1.7.1.js"></script>
<script>
$(function($){
$("#btn").bind("click" , function(){alert("f1");})
.bind("click" , function(){alert("f2");});
});
//當點擊按鈕後彈出窗口依次顯示 f1 f2
</script>
</head>
<body>
<div id="wrapper">
<input id="btn"type="button" value="click me"/>
</div>
</body>
</html>
綁定後緩存熱圖:
通過此圖我們可以看到這跟我們自己實現的緩存方式是非常類似的,只是多了一些額外的東西,比如名稱空間(稍後會講),事件委託。
另外提一個,因爲一個事件類型可以有多個處理器函數,默認情況下,當你取消了冒泡行爲時(event.stopPropagation),這多個處理器還是會全部執行的,如果你想取消冒泡同時當前dom節點未執行的處理器函數也不執行了,則可以調用event. stopImmediatePropagation。
3.3 jQuery事件委託 (delegate)
首先,我們通過一個例子來看下什麼是事件的委託。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="jquery-1.7.1.js"></script>
<script>
$(function($){
var myTable = $("#mytable");
myTable.delegate("tr" , "dblclick", function(){alert("行雙擊");});
$("#add").click(function(){
$("<tr><td>123</td></tr>").appendTo(myTable);
});
//不管你何時點擊"添加"按鈕,都會給表格新增一行
//不管是哪一行,只要在行上雙擊一行,都會彈出窗口顯示 "行雙擊"
//最神奇的是,我們根本沒有顯示給tr添加過"行雙擊"事件,而是通過委託的形式
});
</script>
</head>
<body>
<table id="mytable">
</table>
<input id="add" type="button" value="添加" />
</body>
</html>
委託是種很方便的東西,說得白一點,就是孩子節點的事件處理器放在父節點的事件處理器列表中,讓父節點統一來進行觸發。如果沒有這種機制的話,那麼前面的例子就要單獨給每個tr添加事件了,浪費了不少的空間,而且有時還要寫多代碼。
就上面這個例子我們看下委託後的緩存情況:當我們雙擊了tr時,觸發tr雙擊事件,然後冒泡到table,table根據delegateCount的值進行檢查,因爲delegateCount=1,所以它會對table的事件處理器列表第一個函數(個數由delegateCount決定)進行檢查,看它的selector是否符合當前觸發事件的源dom節點,結果發現剛剛匹配,說明此函數也是要執行的,再與後邊剩餘(總數-delegateCount)的函數列表進行合併,成爲最終的函數處理器列表,這便是事件委託。
最後提一句,在事件的處理上,jQuery1.7提供了兩個堪稱萬能的api,分別是on和off,至於怎麼用,就不多說了,文檔一看就清楚了。
四. 名稱空間
jQuery事件還有一個很新鮮的東西,那就是其獨有的名稱空間了。但是此名稱空間與我們普通想的卻是不太一樣的,來看看jQuery的處理就知道怎麼回事了。
首先介紹一下這個名稱空間。我們在綁定事件時可以這樣來綁定,$(“#btn”).bind(“click.www.jquery.com”, fn); 看到了嗎,在事件類型後邊可以加上”.www.jquery.com”這串東西,這整整一串就稱爲名稱空間。(不要自以爲www是一級,jquery是一級,com是一級),在這裏是沒有多級這種概念的。在保存這個名稱空間的時候,jQuery會這樣做: 把www.jquery.com進行split,得到[“www”,”jquery”,”com”],然後進行sort,得到[“com” , “jquery” , “www”],然後再進行join得到com.jquery.www,最後保存在緩存當中。而當我們利用trigger想觸發某個事件時,比如我們執行$(“#btn”).trigger(“click”),這時候jQuery是這樣處理的:“click”並沒有名稱空間,只有純粹的類型,所以觸發所有類型爲click的函數處理器(不管名稱空間是什麼),但有一個特例,也就是”!”號的使用,如果你剛剛寫的是 $(“#btn”).trigger(“click!”),那麼只會觸發類型爲click,並且名稱空間爲空的函數處理器。
另外一種就是我們觸發有名稱空間的事件了,比如$(“#btn”).trigger(“click.jquery.com”),這時候又怎麼處理呢?
click.jquery.com 的名稱空間爲 jquery.com,split一下,變成[“jquery”,”com”],再sort一下,成[“com” , “jquery”],再join一下,成“com.jquery”,然後依次檢查click類型的處理器,看看它的名稱空間是否匹配”com.jquery”,此例的匹配是這樣的:/(^|\.)com\.(?:.*\.)?jquery(\.|$)/.test(“com.jquery.www”);左邊的//正則是動態產生出來的,而test()中的” com.jquery.www”則是處理器中的名稱空間,只要這個匹配成功了,那麼該處理器便會得到執行。顯然此例子是匹配成功的。關於這個正則就不多說了,並不是很難看懂。
五. 後語
本文主要是對jQuery事件實現方式的一個解說,並展示了自己實現的一個簡單版本的事件處理,希望可以幫助大家對JQuery事件的實現有更好的瞭解。