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規範的瀏覽器,就不會執行對應的捕獲事件監聽器。
- <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>
<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
爲了解決這類不確定性,可以採用一個通用的模式如下:
- node.addEventListener(type, listener, true);
- function listener(evt) {
- if (evt.currentTarget == evt.target) return;
- ...
- }
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規範的理由之一)。可以採用以下模式:
- 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;
- ...
- }
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; ... }
或者 - 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;
- ...
- }
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應 該用於攔截符合條件的子節點事件(許多事件常常僅限於元素),而不是用於一般的事件響應。