天天看點

preventDefault()、stopPropagation()、return false 之間的差別

“return false”之是以被誤用的如此厲害,是因為它看起來像是完成了我們交給它的工作,浏覽器不會再将我們重定向到href中的連結,表單也不會被繼續送出,但這麼做到底有什麼不對呢?

可能在你剛開始學習關于jQuery事件處理時,看到的第一個例子就是關于如何阻止浏覽器執行預設行為,比如下面這段示範click事件的代碼

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">
    $("a.toggle").click(function () {  
        $("#mydiv").toggle(); 
        return false; // Prevent browser from visiting `#` 
    }); 
</pre>      

這個函數使用toggle來顯示或者隐藏#mydiv,然後阻止浏覽器繼續通路href中指定的連結。

像上面這樣的例子會讓使用者養成使用“return false”來阻止浏覽器執行預設行為的壞習慣,在這篇文章裡,我将會讨論關于阻止浏覽器執行預設行為的兩個非常重要的主題:

選擇正确的方法: return false還是preventDefault,stopPropagation或者stopImmediatePropagation

選擇合适的位置,開始,結束,還是中間某個地方:你應該在事件回調的哪個部分取消浏覽器執行預設行為?

注意:當我在這篇文章中提到event bubbling(事件冒泡),我想表達的是大部分事件都是先在初始DOM上觸發,然後再通過DOM樹往上,在每一級父元素上觸發,事件不會在兄弟節點或是子節點上冒泡(當事件向下冒泡時,我們叫它事件捕捉(event capturing)),你可以在這裡了解更多關于事件起泡和捕捉的介紹。

選擇正确的方法

return false之是以被誤用的如此厲害,是因為它看起來像是完成了我們交給它的工作,浏覽器不會再将我們重定向到href中的連結,表單也不會被繼續送出,但這麼做到底有什麼不對呢?

return false到底做了什麼?

當你每次調用”return false“的時候,它實際上做了3件事情:

1、event.preventDefault();

2、event.stopPropagation();

3、停止回調函數執行并立即傳回。

“等等”,你叫了起來!我隻是想讓浏覽器停止繼續執行預設行為而已,我不需要它去做另外2件事。

這3件事中用來阻止浏覽器繼續執行預設行為的隻有preventDefault,除非你想要停止事件冒泡,否則使用return false會為你的代碼埋下很大的隐患,讓我們通過一個真實的例子來看看這樣的誤用會造成什麼後果:

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">
    <div class="post">  
        <h2>
            <a href="http://jb51.net">My Page</a>
        </h2>  
        <div class="content"> Teaser text... </div>  
    </div>  
    <div class="post">  
        <h2>
            <a href="http://jb51.net">My Other Page</a>
        </h2>  
        <div class="content"> Teaser text... </div>  
    </div>  
</pre>      

現在假設我們想要在使用者點選文章标題時,将文章動态載入到div.contentd中:

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">jQuery(document).ready(function ($) {  
  $("div.post h2 a").click(function () { var a    = $(this),  
    href = a.attr('href'), // Let jQuery normalize `href`, 
    content  = a.parent().next();  
    content.load(href + " #content"); return false; // "cancel" the default behavior of following the link 
 });  
}); </pre>      

這段代碼可以正常工作(至少目前是),但如果我們順着這個思路繼續,如果我想要在使用者點選了一個div.post元素(或者任何一個它的子元素)時,給它加上一個active類,我就需要給div.post增加了一個**click回調: **

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">// Inside Document Ready: 
var posts = $("div.post");  
  posts.click(function () { // Remove active from all div.post 
  posts.removeClass("active"); // Add it back to this one 
  $(this).addClass("active");  
}); </pre>      

現在,如果我們點選一個文章的标題,這段代碼會工作嗎?答案是不會,因為我們在标題的click回調裡使用了return false而不是我們應該使用的,”return false“等于event.preventDefault();加event.stopPropagation();,是以事件冒泡就被終止了,click事件不會被冒泡到div.post上,我們為它添加的事件回調當然也就不會被調用了。

如果我們把它和live或者delegate事件混在一起使用時,情況就更糟了。

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">$("a").click(function () { // do something 
  return false;  
});  

$("a").live("click", function () { // THIS WON'T FIRE 
});  </pre>      

那麼我們真正需要的是什麼呢?

1、preventDefault()

大多數情況下,當你使用return false時,你其實真正需要的是e.preventDefault()。要使用e.preventDefault,你需要確定你傳遞了event參數到你的回掉函數中(在這個例子裡,就是那個e):

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">$("a").click(function (e) { // e == our event data 
 e.preventDefault();  
}); </pre>      

它會替我們完成所有工作,但不會阻止父節點繼續處理事件,要記住,你放在代碼中的限制越少,你的代碼就越靈活,也就越易于維護。

2、stopPropagation()

但有些情況下,你有可能需要停止事件冒泡,讓我們看看下面的例子:

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;"><div class="post"> Normal text and then a <a href="http://jb51.net">link</a> and then more text.  
</div>  </pre>      

現在,讓我們假設如果你點了div上除了a連結之外的地方,我們希望能發生點什麼事情(比如改變下背景什麼的),但是不能影響使用者點選a連結的行為(從可用性的角度,這個例子不怎麼好,你可能不希望使用者點選别的地方時發生任何事情)。

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">$("div.post").click(function () { // Do the first thing; 
});  

$("div.post a").click(function (e) { // Don't cancel the browser's default action 
  // and don't bubble this event! 
 e.stopPropagation();  
}); </pre>      

在這種情況下,如果我們使用return false,div的click事件不會被觸發,但是使用者也不會到達他們點的那個連結。

3、stopImmediatePropagation()

這個方法會停止一個事件繼續執行,即使目前的對象上還綁定了其它處理函數,所有綁定在一個對象上的事件會按綁定順序執行,看看下面的例子:

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">$("div a").click(function () { // Do something 
});  

$("div a").click(function (e) { // Do something else 
 e.stopImmediatePropagation();  
});  

$("div a").click(function () { // THIS NEVER FIRES 
});  

$("div").click(function () { // THIS NEVER FIRES 
});  </pre>      

你可能會覺得這個例子看起來很别扭,沒錯,盡管如此,但有時這的确會發生,如果你的代碼非常複雜,那麼不同的widgets和plugin就有可能在同一個對象上添加事件,如果遇到這種情況,那你就很有必要了解和使用stopImmediatePropagation。

4、 return false

隻有當你同時需要preventDefault和stopPropagation,并且你的代碼可以接受直到你的回調執行完成才停止執行浏覽器的預設行為,那你就可以使用”return false“。但我強烈建議你别在寫給其它jQuery開發者的示範代碼中使用這個方法,因為這會造成更多誤用,隻有在你确信非用不可的情況下再去使用”return false“。

選擇适當的位置

如果你使用了”return false“,它隻會在你的回調函數執行結束才去取消浏覽器的預設行為,但是使用e.preventDefault,我們有更多的選擇,它可以随時停止浏覽器執行預設動作,而不管你将它放在函數的哪個部分。

1. 開發階段,你應該總是将它放在第一行。你最不想做的事情可能就是你正在調試将一個form改成ajax送出的時候,它卻已經被按照老方法送出了。

2. 産品階段,如果你采用了漸進增強(progressive enhancement),那就把它放到回調的結束位置,或者是邏輯終點,如果在一個普通頁面采用漸進增強,那你就需要在伺服器端考慮如果浏覽器不支援JS時(或者被禁用時),對連結的click事件和表單的送出事件的處理。這裡的好處是,我們不考慮關閉JS的情況,隻考慮支援js時的強狂,如果你的回調代碼出錯抛出了異常,讓我們看看下面的代碼:

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">var data = {};  
$("a").click(function (e) {  
  e.preventDefault(); // cancel default behavior 
  // Throws an error because `my` is undefined 
  $("body").append(data.my.link); // The original link doesn't work AND the "cool" 
  // JavaScript has broken. The user is left with NOTHING! 
});  </pre>      

現在,讓我們看看同樣的事件,把preventDefault調用放在底部的效果:

<pre style="margin: 0px; white-space: pre-wrap; word-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: "Courier New" !important; font-size: 12px !important;">var data = {};  
$("a").click(function (e) { // Throws an error because `my` is undefined 
  $("body").append(data.my.link); // This line is never reached, and your website 
  // falls back to using the `href` instead of this 
  // "cool" broken JavaScript! 
  e.preventDefault(); // cancel default behavior 
});  </pre>      

這對表單送出也同樣有效,你可以更好的應對出錯的情況,别指望你的代碼一直正常工作,在發生錯誤時有正确的應對總勝過假設代碼不會出錯。

3.在産品階段,如果功能這設計JS,那就還應該放在第一行。

記住,不必非得是函數的第一行,但是越早越好,這裡的原則是:如果函數的功能是通過JS實作的(不涉及服務端互動),那就沒必要考慮相容,在這種情況下,添加在第一行可以防止URL中出現#字元,但顯然,你還是應該盡可能多的增加些錯誤處理代碼,以防止使用者在出錯時變得不知所措。

繼續閱讀