這次來討論一個已經很久但可能有些人還沒掌握、理解的觀念: event delegate。
有些人或許不明白什麼是 delegate,其實概念上他是很簡單的。
delegate 絕對不是個新概念,早在 jQuery 1.1.3 時代就已經有許多人透過 livequery jQuery plugin 操作 delegate 。只是因為他的觀念不容易直接解釋,所以會讓人不太理解 delegate 的意義在哪。
Event delegate 是基於 event binding 上的一種技巧,當你能夠瞭解 event delegate ,你自然就能掌握何時該用 delegate,何時該用 event binding ,然後你會發現有些情境底下,你的確應該優先考慮 event delegate 而不是 event binding。
照傳統的作法,這時候我們會需要去說明什麼是 delegate ,他跟 event binding 的差別在哪,但這樣不容易讓使用者感覺到他的差異。我計畫換個角度來講這個問題,先講 "event" 。:)
@ event
event 是很簡單的概念,但是因為它太常見所以我們其實不太容易去介紹他。
這裡我們講得 event 大多說的是 dom event,也就是"使用者對畫面元素做了某些事情時,如點擊、滑鼠移過去..etc 時觸發的事件",dom event 本身只是一個通知的機制。
打比方來講,像是我們用茶壺煮開水,水煮開時,有些茶壺會發出笛聲通知通知我們去處理以免水滾過頭,這就有點像是事件的觸發,笛聲可以"通知"我們去處理事件。
在這裡如果從程式的角度來看,我們可以說茶壺註冊了煮沸事件,煮沸時發出笛聲,
我們也可以說人註冊煮沸事件,聽到笛聲時去進行後續的動作,
但如果你沒有註冊事件,這些事件依然存在。
假設水煮沸時沒有人在家,笛聲依然會響、水依然會滾。
我們在這裡不介紹事件的種類跟可以有多少種種類,只介紹網頁中最常用的事件,click 事件。
當我們用滑鼠點擊畫面中的任何元素(文字、圖片、按鈕等)都會出發 click 事件,
不管有沒有人去處理這個事件,他都會發生。
常見用途有「點擊跳出確認視窗」、「點擊以編輯」等。
如底下 jQuery base 的範例
<div id="test">點我顯示對話框</div>
<script type='text/javascript' src='//code.jquery.com/jquery-1.9.1.js'></script>
<script>
$("#test").on("click",function(){
alert("hi, click is fired.");
});
</script>
測試網址
http://jsfiddle.net/GvAqz/
click 事件是一個觸發點,你可以使用一個 JS 的 function ,
去處理當這個事件發生之後他要做什麼事情,我們稱之為處理函式(handler),
所以知道各類事件只能讓你知道如何、何時、可以收到通知,但處理還是各使用者該自己進行的。
也因為絕大多數操作都來自於事件的這個特性,讓 JS 被稱為是 event-driven (事件驅動)的環境。
@ event target
這就是今天的重頭戲,也是 event delegate 跟 event binding 最大的差別。
dom event 是產生在 dom 元件上的事件,所以每個事件都會有 dom 元素的參與,像以上面的例子,我們就是觸發了 div 的點擊行為。我們範例程式碼中也註冊 div 的 click 事件進行處理。
$(dom).on("click",function(){
//handler code
});
這就是一個完整的 binding sample ,也是大家所熟知最最基本的 event binding ,幾乎不可能有人不懂怎麼做 event binding 。
那根據之前的資料,這聽起來非常的直覺,為什麼我們還需要 event delegate ?
正因為所有操作跟所有事件都能透過 event binding 處理,所以很多人不會去思考 event delegate 的存在與運用 event delegate ,event binding 在所有情境下都是適用的。
但 event delegate 存在總是有理由,我會寫這篇文章也不是要介紹個沒用的東西,
所以我們得來證明有些時候 event binding 是蠢的,對吧? :)
我先不急著說明 event delegate 的存在,我們接下來會來說明 event binding 發展的過程。
@ 從單一元素綁定(bind)到列表元素綁定
一開始的狀況很簡單,我們有非常多的事件要綁定,假設是清單畫面,
因為我們要針對清單的所有列表做出處理,就必須要一一 bind 許多元素。
如這個例子
<table id="test">
<tr><td>List item1</td><td><input type="button" value="edit" data-index="1" /></td></tr>
<tr><td>List item2</td><td><input type="button" value="edit" data-index="2" /></td></tr>
<tr><td>List item3</td><td><input type="button" value="edit" data-index="3" /></td></tr>
<tr><td>List item4</td><td><input type="button" value="edit" data-index="4" /></td></tr>
<tr><td>List item5</td><td><input type="button" value="edit" data-index="5" /></td></tr>
<tr><td>List item6</td><td><input type="button" value="edit" data-index="6" /></td></tr>
<tr><td>List item7</td><td><input type="button" value="edit" data-index="7" /></td></tr>
</table>
<script type='text/javascript' src='//code.jquery.com/jquery-1.9.1.js'></script>
<script>
$("#test [type=button]").on("click",function(){
alert("hi, edit is fired on row "+$(this).data("index"));
});
/* Almost same as
var inputs = $("#test [type=button]");
for(var i = 0 ; i < inputs.length;++i){
inputs.eq(i).on("click",function(){
alert("hi, edit is fired on row "+$(this).data("index"));
});
}
*/
</script>
測試網址 http://jsfiddle.net/KkNFy/1/
這裡我們看到一個問題,我們要 bind 大量元素,但這其實是個小問題,反正 jQuery 幫我們做了 each 跟 bind 的部份,大部分人也不是真的那麼在意 bind 的 resource issue,所以這個問題是小問題、不是主要的問題,所以到這裡我們還是會建議用 event binding 。
@ 從列表元素綁定到動態元素綁定
接下來因為動態網頁的關係,我們可能透過 ajax 、可能透過事件的關係增減列表元素,
如這個例子,很多人會發現新增後的元素沒有辦法觸發 click 事件,因為他們並沒有被綁定事件,但我們會直覺以為我們是綁定整個清單的元素,而不是一個個的元素,而導致誤解:
<table id="test">
<tr><td>List item1</td><td><input type="button" value="edit" data-index="1" /></td></tr>
<tr><td>List item2</td><td><input type="button" value="edit" data-index="2" /></td></tr>
<tr><td>List item3</td><td><input type="button" value="edit" data-index="3" /></td></tr>
<tr><td>List item4</td><td><input type="button" value="edit" data-index="4" /></td></tr>
<tr><td>List item5</td><td><input type="button" value="edit" data-index="5" /></td></tr>
<tr><td>List item6</td><td><input type="button" value="edit" data-index="6" /></td></tr>
<tr><td>List item7</td><td><input type="button" value="edit" data-index="7" /></td></tr>
</table>
<div>
<input type="text" id="input" value="new item" />
<input type="button" id="btn" value="add item" />
</div>
<script type='text/javascript' src='//code.jquery.com/jquery-1.9.1.js'></script>
<script>
$("#test [type=button]").on("click",function(){
alert("hi, edit is fired on row "+$(this).data("index"));
});
var index = $("#test [type=button]").length; //get last index
$("#btn").click(function(){
index++;
var html = $("#input").val(); //Note:Here might be xss issue, so this is just for example.
$("#test").append('<tr><td>'+html+'</td><td><input type="button" value="edit" data-index="'+index+'" /></td></tr>');
});
</script>
測試網址 http://jsfiddle.net/KkNFy/2/
@ event binding COULD solve the problem
是的,我們說過, event binding 可以解決大多數的問題,所以我們會看到各式各樣的解法。
像是
<script>
$("#test [type=button]").on("click",function(){
alert("hi, edit is fired on row "+$(this).data("index"));
});
var index = $("#test [type=button]").length; //get last index
$("#btn").click(function(){
index++;
var html = $("#input").val(); //Note:Here might be xss issue, so this is just for example.
$("#test").append('<tr><td>'+html+'</td><td><input type="button" value="edit" data-index="'+index+'" /></td></tr>');
//bind the event again .
$("#test [type=button]").on("click",function(){
alert("hi, edit is fired on row "+$(this).data("index"));
});
});
</script>
(測試網址 http://jsfiddle.net/KkNFy/3/)
接著實驗一下,新的 item 的確會 alert 了,但是舊得 item alert 變成多次了,
因為每次 add 時我們的程式碼是針對所有 td 裡面的 input 又重新 bind,所以事件會累加。
顯然這是個"錯誤"的結果。
接下來又有各類各種強者實作,如這個取得最後一個元件來 binding 的模型(只在這個案例有用)
<script>
$("#test [type=button]").on("click",function(){
alert("hi, edit is fired on row "+$(this).data("index"));
});
var index = $("#test [type=button]").length; //get last index
$("#btn").click(function(){
index++;
var html = $("#input").val(); //Note:Here might be xss issue, so this is just for example.
$("#test").append('<tr><td>'+html+'</td><td><input type="button" value="edit" data-index="'+index+'" /></td></tr>');
$("#test [type=button]:last").on("click",function(){
alert("hi, edit is fired on row "+$(this).data("index"));
});
});
</script>
測試網址 http://jsfiddle.net/KkNFy/4/
這個強者實作則是先取消原本事件,再重新綁定新的事件:
<script>
$("#test [type=button]").on("click.showalert",function(){
//use showlaert event namespace to prevent off unexpected event
alert("hi, edit is fired on row "+$(this).data("index"));
});
var index = $("#test [type=button]").length; //get last index
$("#btn").click(function(){
index++;
var html = $("#input").val(); //Note:Here might be xss issue, so this is just for example.
$("#test").append('<tr><td>'+html+'</td><td><input type="button" value="edit" data-index="'+index+'" /></td></tr>');
$("#test [type=button]").off("click.showalert").on("click.showalert",function(){
alert("hi, eidt is fired on row "+$(this).data("index"));
});
});
</script>
測試網址 http://jsfiddle.net/KkNFy/5/
以上這些實作都是筆者曾經自己實作或看過人做過得作法。因為他的確會運作,所以許多人選擇用這種方式去進行運作,但其實有更聰明的方式,也就是 event delegate 可以用。
只要將最一開始的版本調整一下,就能輕鬆支援動態物件變更:
<script>
$("#test").on("click","[type=button]",function(){
alert("hi, edit is fired on row "+$(this).data("index"));
});
var index = $("#test [type=button]").length; //get last index
$("#btn").click(function(){
index++;
var html = $("#input").val(); //Note:Here might be xss issue, so this is just for example.
$("#test").append('<tr><td>'+html+'</td><td><input type="button" value="edit" data-index="'+index+'" /></td></tr>');
});
</script>
測試網址http://jsfiddle.net/KkNFy/6/
現在不管新增多少個他都能正常運作囉,不會多不會少,比起其他實作是不是覺得簡單多了?^^
@ 這實作看起來跟 event binding 好像,只是 selector 跟參數有點不一樣?
其實在操作上他們的感覺幾乎是一樣的,差別在於實作層,剛剛的範例中 event delegate 實際上將 click 事件註冊在 #test (table) 上,所以只有一個元素註冊 click 事件。
而所有動態新增的 tr/td/input 都在這個 table 的子元素(child)範圍內,
所以他們都會受到這個註冊事件的影響。
@ 等等,我註冊 table 的 click 事件跟 table 子元素內 input 的 click 有什麼關係?
因為 event 有向父元素傳遞的特性,點擊 input 實際上因為 input 在 table 內,所以他也可以說是點擊 table ,所以 table 的 click 事件會受到觸發,而我們可以透過事件取得到底是哪個子元素觸發這個事件。
承上 delegate 就是透過這個原理,將 table 的 click 事件篩選(filte)出滿足我們([type=button]) 條件的元素,並且在滿足條件式時才觸發 table 上的 click delegate 事件處理器,讓我們有明明就綁定在 table 卻像是綁定在 input 上的錯覺。
與其一一管理底下的子事件,透過上層父元素統一分配事件的作法,在這類動態新增的元素中特別有效,而這就是 event delegate 的核心精神,先往上傳遞再讓父元素去分派,到這裡,理論上你應該已經能完全瞭解 delegate 的功用了。
@ 既然都會往上傳遞,那我們為什麼不 delegate 在 document (最上層元素) 上?
$("#test").on("click","[type=input]",cb);
對上
$(document).on("click","[type=input]",cb);
前者也會有當 #test 被移除時或 #test 也是動態新增的情況下,會有沒有 binding 的問題,
所以很多人會直接乾脆都用 document 或 body 來進行 binding 。
在過去我們是直接稱為這種綁定在最核心元素的 delegate 事件是 live 事件,也就是所有元素只要滿足條件都會觸發,早期相當方便所以很多人會使用,但經過長期使用下來,
這裡我個人會強力建議大家避免使用 document 作為 delegate dom ,有幾個理由:
1.事件分派需要時間,假設有十個 live click 事件,表示畫面上所有元素點擊時都要判斷那些元素有沒有滿足這十個 selector,這成本會非常的高,隨著你註冊事件數量的多寡呈現接近指數型的成長。
2.核心元素意味著包含得內容元素也多,可能你原本 #test 內的 .author 只有一個,但 document 的 .author 有很多個,在操作時下 selector 會需要變得比較複雜以免不小心誤中,當然也會因此增加因為 name conflict 帶來的風險。
其中影響最大的還是效能問題,另外你應該要全局考慮一下,當 #test 會被移除時,頁面整體的事件載入策略,而不是貪圖方便就輕易的把事件綁定在 document,要知道、今天的方便就是明天動彈不得的泥沼。
delegate 事件在許多動態、重複性的事件中都能發揮比正常 binding 更簡單、更輕鬆、更好掌握、更低資源的作法,當然不是說所有 binding 事件都要用 delegate 取代,但是知道並瞭解他的存在,絕對可以幫我們省下更多時間並在某些情境下可以大幅減少系統複雜度。:)
其實還有一些使用 delegate 時需要注意的問題,但相信當你開始採用並瞭解 delegate 之後,你自然就能理解這些問題的理由跟知道如何應對。(像是對 click 作 cancel bubble event 為什麼沒有防到 parent click 之類的。:p)