使用捕獲事件監聽器(useCapture=true)的陷阱及其對策

本文原發於我在JavaEye的blog上


DOM event flow有三個phase,capture、target和bubble。通常我們只在後兩個階段處理事件,也即在調用addEventListener (type, listener, useCapture)時,useCapture設爲false。偶爾可能會使用所謂捕獲事件監聽器(Capturing Event Listeners),即useCapture設爲true。但有一個很搞的問題,那就是在event.currentTarget等於 event.target的時候(即event flow處於target phase時),是否會調用添加到currentTarget上的useCapture爲true的listener?

不同瀏覽器在這一點上存在分歧。各版本的Firefox和最新版本的Safari會調用,而Opera和老版本的Safari就不會調用。

有人認爲DOM Level 2事件規範在這一點上存有歧義,但如果仔細分析,可以確定DOM 2規範的意思確實是不應在目標階段(target phase)調用捕獲事件監聽器,W3C發佈的測試套件(test suite)也測試了這一點,DOM 3事件規範的草案也再次明確了這一點。

然而基於種種原因,所有版本的Gecko引擎(Firefox等瀏覽器)和最近版本的WebKit引擎(Safari等瀏覽器)都會在目標階段調用包括捕獲事件監聽器在內的所有監聽器,並且有些人認爲應該據此修改DOM規範。這種看法確實也存在一定的合理性。

具體情況可參考:
https://bugzilla.mozilla.org/show_bug.cgi?id=235441
http://bugs.webkit.org/show_bug.cgi?id=9127

總之,這一問題在短期內可能不會有確定的結果。無論如何,在使用捕獲處理器時應注意避免這一不確定行爲的影響。

Gotchas

元素所對應的區域如果有一部分不在任何子元素內(如文本節點),嚴格遵循DOM規範的瀏覽器,就不會執行對應的捕獲事件監聽器。

Html代碼 
  1. <div id="div1">  
  2. As DOM spec, <strong>Only this area</strong> will trigger  
  3. the capturing event listener for click event on the containing  
  4. div element.  
  5. </div>  
  6. <script>  
  7. var div1 = document.getElementById('div1');  
  8. div1.addEventListener('click', function () { alert('ok'); }, true);  
  9. </script>  
<div id="div1"> As DOM spec, <strong>Only this area</strong> will trigger the capturing event listener for click event on the containing div element. </div> <script> var div1 = document.getElementById('div1'); div1.addEventListener('click', function () { alert('ok'); }, true); </script> 
又如,如果同一個監聽器,在同一個元素上註冊兩次,一次useCapture爲true,一次爲false,那麼在嚴格遵循DOM規範的瀏覽器 中,監聽器在整個event flow中只會被調用一次;反之則會被連續調用兩次,而且函數自身是無法判斷到底是作爲捕獲事件監聽器被調用,還是作爲非捕獲事件監聽器被調用。當然,實 際上是先調用捕獲事件監聽器再調用非捕獲事件監聽器的,但是如果加上target對象上的event handler(即onclick之類的事件屬性),就又產生了微妙的順序問題。Gecko的順序是先執行handler再執行監聽器,而WebKit的 順序是先執行捕獲事件監聽器,再執行handler,最後執行非捕獲事件監聽器。

Workaround

爲了解決這類不確定性,可以採用一個通用的模式如下:

Javascript代碼 
  1. node.addEventListener(type, listener, true);  
  2.   
  3. function listener(evt) {  
  4.   if (evt.currentTarget == evt.target) return;  
  5.   ...  
  6. }  
node.addEventListener(type, listener, true);  function listener(evt) {   if (evt.currentTarget == evt.target) return;   ... } 
這樣就確保了不會在target階段執行捕獲事件監聽器。我們也可以判斷 evt.eventPhase != evt.CAPTURING_PHASE,但是瀏覽器的eventPhase也可能有bug,所以最好直接判斷currentTarget是否等於 target。

此外,有些時候我們反而希望確保在target階段執行(這也正是認爲應該修改DOM規範的理由之一)。可以採用以下模式:

Javascript代碼 
  1. node.addEventListener(type, listener1, true);  
  2. node.addEventListener(type, listener2, false);  
  3.   
  4. function listener1(evt) {  
  5.   if (evt.currentTarget == evt.target) return;  
  6.   ...  
  7. }  
  8. function listener2(evt) {  
  9.   if (evt.currentTarget != evt.target) return;  
  10.   ...  
  11. }  
node.addEventListener(type, listener1, true); node.addEventListener(type, listener2, false);  function listener1(evt) {   if (evt.currentTarget == evt.target) return;   ... } function listener2(evt) {   if (evt.currentTarget != evt.target) return;   ... } 
或者

Javascript代碼 
  1. node.addEventListener(type, listener, true);  
  2. node.parentNode.addEventListener(type, listener, true);  
  3.   
  4. function listener(evt) {  
  5.   if (evt.currentTarget == node.parentNode && evt.target != node  
  6.   || evt.currentTarget == evt.target) return;  
  7.   ...  
  8. }  
node.addEventListener(type, listener, true); node.parentNode.addEventListener(type, listener, true);  function listener(evt) {   if (evt.currentTarget == node.parentNode && evt.target != node   || evt.currentTarget == evt.target) return;   ... } 
在執行順序上,前者類似Gecko的行爲,後者類似WebKit的行爲。

而且最好不要使用後者,因爲它強制要求事件監聽器引用target節點,從而構成了閉包,這降低了listener的可重用性。

總的來說,我建議應儘可能避免使用useCapture=true,因爲絕大多數需求都應在target和bubbling階段處理,特別是涉及 UI的事件。如果確實有必要使用捕獲事件處理器,應優先考慮符合當前DOM規範的約束,即不在target階段執行它。這意味着,useCapture應 該用於攔截符合條件的子節點事件(許多事件常常僅限於元素),而不是用於一般的事件響應。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章