理解JS的事件委託
首先我們來看一下下面的例子:
假設我現在有一個button列表,當我點擊他們其中的某一個時,我將會對他們標記一個"active"類,當我再點擊一次,則刪除這個類,下面是html代碼:
<ul class="toolbar"> <li><button class="btn">Pencil</button></li> <li><button class="btn">Pen</button></li> <li><button class="btn">Eraser</button></li> </ul>
我們可以使用原生的JS來做這樣子的效果:
1 var buttons = document.querySelectorAll(".toolbar .btn"); 2 3 for(var i = 0; i < buttons.length; i++) { 4 var button = buttons[i]; 5 button.addEventListener("click", function() { 6 if(!button.classList.contains("active")) 7 button.classList.add("active"); 8 else 9 button.classList.remove("active"); 10 }); 11 }
看起來JS是OK的,但是不起作用,達不到我們所要的效果,這是為什麼呢?
都是閉包惹的禍
對於有一定JS基礎的人來說,原因是很明顯的--都是閉包惹的禍。
我們可以看到,綁定的function里用到了button變量,而button變量只有一個,在for循環的每一次循環里,它的值都會被重新分配.
在循環的第一次里,button指向了第一個按鈕,下次循環時指向了第二個按鈕......當你開始點擊按鈕時,循環已經結束了,button指向了最後一個按鈕,所以我們點擊了按鈕最終操作的還是最後一個button。
我們所需要的是一個固定作用域,讓我們來重構一下我們的function,如下
1 var buttons = document.querySelectorAll(".toolbar button"); 2 var createToolbarButtonHandler = function(button) { 3 return function() { 4 if(!button.classList.contains("active")) 5 button.classList.add("active"); 6 else 7 button.classList.remove("active"); 8 }; 9 }; 10 11 for(var i = 0; i < buttons.length; i++) { 12 buttons[i].addEventListener("click", createToolBarButtonHandler(buttons[i])); 13 }
看起來好些了,而且現在它也工作正常了。我們使用了一個方法來給button創建了一個穩固的作用域,在處理的函數里,button總會指向我們所需要的按鈕。
但是,是不是還有問題?
在大多數的情況下,上述的處理方法都是OK的,不過,我們可以做得更好。
首先,我們創建了太多的處理函數,對於每個匹配.toolbar 的 button 我們都創造了一個function與event listener關聯。對於三個按鈕來說,這點是微不足道的。
但是假設有下列的情況:
<ul class="toolbar"> <li><button id="button_0001">Foo</button></li> <li><button id="button_0002">Bar</button></li> // ... 997 more elements ... <li><button id="button_1000">baz</button></li> </ul>
它不會爆炸,但是如果我們還這麼處理的話就會顯得不大好了。我們將會分配大量的函數出來,每次都要將目標按鈕作為參數傳入。但其實我們沒必要和麼做。
與其使用button變量來追蹤我們點擊的是哪一個按鈕,我們可以用event對象作為參數實現。
event對象包含了許多關於時間的數據。在這個情況下,我們需要用currentTarget屬性去得到我們所點擊的按鈕對象。
1 var button=querySelectorAll(".toolbar button"); 2 3 var toolBarButtonHandler = function(e){ 4 var button = e.currentTarget; 5 if(!button.classList.contains("active")) 6 button.classList.add("active"); 7 else 8 button.classList.remove("avtive"); 9 }; 10 11 for(var i<0; i < button.length; i++){ 12 button.addEventListener("click", toolBarButtonHandler); 13 }
這下看起來好些了,我們需要綁定同一個方法多次就可以了,而且代碼的可讀性也比較高
但是,我們還是可以做得更好
假設我們動態地增加了幾個按鈕到列表裡面。然後我們就必須得去動態地再去綁定多一些事件到按鈕裡面。這聽起來有些麻煩
也許會有一些其他的好方法?
現在讓我們一起來探討一下時間在DOM裡面是如何運作的
事件是如何運作的
當用戶點擊了一個按鈕,一個事件將會被產生以此來相應用戶。而事件的派遣將會涉及到一下三個方面:
- 捕獲
- 目標
- 冒泡
這裡請注意一下,不是所有事件都有捕獲/冒泡的過程,可能它們直接被派遣到目標上,但是絕大多數情況下是有的
事件從document外部開始,然後穿過DOM,直到目標事件上,接著開始原路返回,直到退出DOM
我們看一下下面的HTML:
1 <html> 2 <body> 3 <ul> 4 <li id="li_1"><button id="button_1">Button A</button></li> 5 <li id="li_2"><button id="button_2">Button B</button></li> 6 <li id="li_3"><button id="button_3">Button C</button></li> 7 </ul> 8 </body> 9 </html>
當用戶點擊了按鈕1,則事件將會這麼發展下去:
開始
| #document \
| HTML |
| BODY } 捕獲
| UL |
| LI#li_1 /
| BUTTON <-- 目標
| LI#li_1 \
| UL |
| BODY } 冒泡
| HTML |
v #document /
結束
請注意到從事件點擊的開始到結束這個過程實際上我們是可以去跟蹤的。 我們可以看到,任何一個按鈕,只要我們點擊,最終它的時間都將會冒泡的它們父親節點ul中。我們可以從事件派遣的這個特點入手,將我們的點擊時間委託到父親節點中
事件委託
事件委託即是將事件綁定到父親元素上,但當事件的目標符合某些條件才會執行。
請看下面的HTML
<ul class="toolbar"> <li><button class="btn">Pencil</button></li> <li><button class="btn">Pen</button></li> <li><button class="btn">Eraser</button></li> </ul>
我們已經知道了無論我們點擊哪一個按鈕,事件最終都會冒泡到它的父親元素ul.toolbar上,所以讓我們在上面放置事件。
var toolbar = docuemtn.querySelectorAll(".toolbar"); toolbar.addEventListener("click",function(e){ var button = e.target; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); });
這樣子處理我們少了許多代碼,而且我們不再需要循環了。這裡請注意到我們用e.target替代了e.currentTarget。因為我們現在監聽的事件是在不同的層中。
- e.target是事件的實際目標.在DOM裡面實際事件所來源或者所需要獲取的對象
- e.currentTarget是處理事件的元素
上述情況,e.currentTarget將會是ul.toolbar
更茁壯的事件委託
我們已經通過冒泡來處理對ul.toolbar的點擊事件了,但是我們的匹配策略太過簡單了。如果在ul裡面有其他元素我們不想被點擊的話怎麼辦呢?
<ul class="toolbar"> <li><button class="btn"><i class="fa fa-pencil"></i> Pencil</button></li> <li><button class="btn"><i class="fa fa-paint-brush"></i> Pen</button></li> <li class="separator"></li> <li><button class="btn"><i class="fa fa-eraser"></i> Eraser</button></li> </ul>
如上,當我們點擊了li#separator或者其他的icons,我們將會為他們添加active這一個類。這可不大好。我們需要過濾掉這些不符合的元素。
我們可以對JS進行修改:
var delegate = function(criteria, listener) { return function(e){ var el = e.target; do{ if(!criteria(el)) continue; e.delegateTarget = el; listener.apply(this, arguments); return; } while( (el = el.parentNode) ); }; };
上面的JS首先遍歷了每一個元素包括他們的父親節點看是否匹配criteria函數。如果匹配了,它將會在對象上增加一個delegateTarget屬性,這個屬性指向匹配criteria函數的對象。對於不匹配的,則沒有事件發生。
我們可以這麼使用它:
var toolbar = document.querySelector(".toolbar"); var buttonsFilter = function(elem) {return elem.classList && elem.classList.contains("btn");}; var buttonHandler = function(e){ var button = e.delegateTarget; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }; toolbar.addEventListener("click", delegate(buttonsFilter, buttonHandler));
看來我們的目的達到了!一個簡單的事件處理,關聯到單一的元素上,只有匹配的元素才會增加或者刪除active類。並且我們可以不用去考慮ul元素裡面以後時候會動態地去增加多一些按鈕了!
參考:http://codepen.io/32bitkid/blog/understanding-delegated-javascript-events