he Dangers of Stopping Event Propagation
作者:Philip Walton
PS:這是我之前學習DOM時看到了一篇關于濫用event.stopPropagation();的壞處的文章,覺得很棒,便翻譯下來。
PS:適合正在學習DOM的前端新手,和想對DOM加深理解的JSer們。O(∩_∩)O
他將為我們解釋為什么stopPragation()并不是一個明智的方法,并且你最好完全的避免使用這一方法。
如果你是一位前端工程師,在某些時候你沒準要在頁面實現這樣的功能:在用戶點擊頁面的其余位置后,彈出框或對話框便消失。
如果你通過網上搜索去查明如何實現這一功能,你有很大可能性找到Stack Overflow中獲得最高贊答案的代碼:
jQuery
$('html').click(function() {
// Hide the menus if visible.
});
$('#menucontainer').click(function(event){
event.stopPropagation();
});
防止大家不是很明白這行代碼的作用,這里是一個簡單的綱要:
如果一個點擊事件冒泡向元素,隱藏菜單。如果那個單擊事件是從#menucontainer內部發起的,停止冒泡這樣他就不會在冒泡向,因此只有外界的單擊事件才會觸發這一事件。
上一段代碼是簡單優雅的并且很聰明。但是,不幸的是,他也是一個非常槽糕的建議。
這一方案之粗糙就像是你為了修理壞掉的淋浴頭而把浴室的水源切斷了。
他的確行之有效,但是它完全阻斷了這一頁面其他代碼調用這一事件的可能性。
盡管如此,他還是Stack Overflow下這一問題得贊最多的答案,說明人們承認這是一個合理的建議。(Philip君內心吐槽:這么low的代碼你們還贊!)
會出現什么問題?
你也許會這樣想:誰還會寫這樣的代碼?我使用了一個很好的庫像Bootstrap,所以我不用擔心這個是嗎?
不幸的是,并不。停止事件冒泡并不只是被Stack Overflow推薦的壞答案;他也出現在一些最流行的庫中。
為了證明這一點,讓我給你展示在Ruby on Rails app中使用Bootstrap是多么容易發生BUG。包含JavaScript庫的Rails ships 被稱為jquery-ujs,它允許開發者去說明式地添加移除通過data-remote屬性的AJAX聲明連接。
(這段的翻譯有問題,畢竟我不是很懂Ruby,但是并不影響文章理解O(∩_∩)O )
在下面的例子中,如果你打開dropdown控件并且點擊frame窗口的其他任意位置,dropdown控件都會關閉,但是當你打開dropdown后再去點擊”remote Link”,它就不會關閉了。
例子在CODEPEN網站里,這里是鏈接.
這一bug的發生是因為Bootstrap只關閉那些監聽了document屬性里click事件的dropdown菜單。
但是由于ujs在它的data-remote鏈接操作里停止了事件冒泡,這些點擊事件就再也不會到達document屬性了,
因此Bootstrap的代碼就不會再運行了。最差勁的地方就是在Bootstrap(或者任何框架里)不能提供任何幫助去阻止這一BUG。如果你在處理DOM,你只能任憑其他不嚴謹的代碼在頁面上運行。
事件的問題
就像許多在JavaScript中的事情一樣,DOM事件是全局性的。并且就像大多數人知道的那樣,全局變量會帶來凌亂、耦合的代碼。
修飾一個單一的、短暫的事件乍一看上去也許會是無害的,但是,它帶來了風險。
當你改變一個人們期待和其他代碼依賴的行為屬性,你就將獲得bug(%>_<%),這只是一個時間問題。
并且在我的經驗里,這一種的BUG是最難去捕獲到的。
為什么人們使用停止事件冒泡這一方式?
我們知道在網上有推廣著使用stopPropagation無用性的不好的建議,但是這不是唯一的理由人們做這個。開發者時常無意識地停止事件冒泡。
返回false值
有無數的混亂圍繞著你當你從一個事件句柄中返回false時。考慮下列三種情形:
HTML
<!-- An inline event handler. -->
<a onclick="return false">Google</a>
jQuery
// A jQuery event handler.
$('a').on('click', function() {
return false;
});
JavaScript
// A native event handler.
var link=document.querySelector('a');
link.addEventListener('click', function() {
return false;
});
這三個例子看來都想做同樣的事情(只是返回false),但是在現實中返回的結果卻是不一樣的。這里是實際上會發生在上面
的情況中情形:
1.從一個內聯事件句柄返回false阻止了瀏覽器操縱連接地址,但是不會阻止從DOM冒泡而來的事件。
2.從一個jQuery事件句柄返回false阻止了瀏覽器操縱連接地址,也阻止了會停止從DOM冒泡而來的事件。
3.從一個正常DOM事件句柄返回的false完全不會有任何作用
當你期望一些事件的發生但是并沒有時,這是令人困惑的,但是你通常能立刻捕獲它。一個更大的問題是當你期望一些事發生它也如你所愿,但是它帶來了意料之外不顯眼的副作用。這就是那些可怕BUG的來由。在jQuery案例中,我們已經清楚跟另外兩個事件句柄相比返回false一點也不會帶來什么不同的行為,但是它有。
在后臺,jQuery實際上調用了下面兩個語句:
jQuery
event.preventDefault();
event.stopPropagation();
明確你的意圖和直接調用這些事件方法是更好的選擇。因為圍繞著返回false的困惑和它停止事件冒泡在jQuery事件句柄的這一事實,我建議最好決不使用它。
注意:如果你將jQuery和CoffeeScript一起使用(自動返回函數的最后一個表達式),確保你沒有使用任何評估為false的布爾值結束你的事件句柄不然你會有同樣的問題。
性能
時常你會讀一些忠告(通常寫于很久之前),建議為了性能而停止冒泡。
在IE6還盛行的日子(前端第一大頭疼╭(╯^╰)╮)甚至更老式瀏覽器還流行的時候,一個復雜DOM真的可以
減緩你的網站性能。由于事件經過整個DOM,節點越多,得到任何事件的時間越慢。
quirksmode.org的作者Peter Paul Koch 建議了下面的做法在一篇老文章的主題上:
如果你的文檔結構是非常復雜的(很多嵌套表等),你可能會為了節省系統資源而關閉冒泡。瀏覽器必須經過事件目標的每一個父元素,看它是否有一個事件句柄。即使沒有發現,搜索仍需占用時間。
這是一個微優化而不是你的性能瓶頸。在現在瀏覽器中,任何你通過阻止事件冒泡得到的性能提升都有可能會被你的用戶忽視。
我的建議是不必擔心通過整個DOM的事件冒泡。畢竟,這是規范的一部分,而且瀏覽器已經很擅長這樣做。
怎樣去替代呢?
作為一項基本規則,停止事件冒泡不能作為一個方案去解決一個問題。如果你的網頁上多個事件句柄,并且有時相互干擾,然后你發現阻止冒泡可以解決一切問題,這是一件壞事。它可能會解決你當下的問題,但它可能創建另一個你不知道的。
阻止冒泡應該被認為像取消一個事件,它也只能使用于那種目的。
也許你想阻止表單提交或不允許關注頁面的一個區域。在這些情況下你阻止冒泡,因為你不想讓一個事件發生,不是因為你有一個不想要的DOM事件句柄被向上傳遞了。
在“如何在元素之外的檢測一個點擊事件?”的例子里,調用stopPropagation的目的并不是要完全除去單擊事件,這是為了避免在頁面上運行一些別的代碼。
這是一個壞主意除了因為它改變了全局行為,還因為它將菜單隱藏邏輯放在在兩個不同的和無關的地方,使它比需要的更脆弱。
一個更好的解決方案是有一個單獨的事件句柄其邏輯是完全封裝,而它唯一的作用是通過給出的事件判斷是否隱藏菜單。
事實證明,更好的選擇最終需要更少的代碼:
jQuery
$(document).on('click', function(event) {
if (!$(event.target).closest('#menucontainer').length) {
// Hide the menus.
}
});
上述句柄監聽document上的單擊事件并檢查事件的目標是否為#menucontainer或# menucontainer的父節點。如果沒有,你知道點擊起源于# menucontainer之外,因此你可以隱藏菜單如果他們是可見的。
默認阻止?
大約一年前,我開始寫一個事件句柄庫來幫助處理這個問題。而不是阻止事件冒泡,你也許會把一個事件簡單的標記為“被處理的”。這將允許事件監聽器注冊更遠的DOM事件去檢查事件,根據是否被“處理”,確定進一步的行動是否為必要的。我的想法是,你可以“停止事件冒泡”然而實際上沒有停止它。
最終結果是,我從來沒有需要這個庫。在100%的情況下當我發現自己想要檢查一個事件是否被“處理”,我注意到一個叫做preventDefault的早先的偵聽器。和DOM API已經提供了一種方法來檢查:defaultPrevented屬性。
幫助澄清這一點,我提供了一個例子。
想象你將一個事件監聽器添加到文檔,將使用谷歌分析跟蹤用戶點擊鏈接到外部域。它可能看起來像這樣:
jQuery
$(document).on('click', 'a', function(event) {
if (this.hostname !='css-tricks.com') {
ga('send', 'event', 'Outbound Link', this.href);
}
});
這段代碼的問題是,并非所有的鏈接點擊帶你去其他頁面。有時JavaScript會攔截點擊,調用preventDefault和做其他的事情。上述數據遠程鏈接是一個典型的例子。另一個例子是一個Twitter分享按鈕,打開一個彈出框而不是跳去twitter.com。
為了避免跟蹤這些點擊,你可能想要停止事件冒泡,但是檢查defaultPrevented事件是一個更好的方式。
jQuery
$(document).on('click', 'a', function(event) {
// Ignore this event if preventDefault has been called.
if (event.defaultPrevented) return;
if (this.hostname !='css-tricks.com') {
ga('send', 'event', 'Outbound Link', this.href);
}
});
由于在點擊句柄中調用preventDefault總是阻止瀏覽器導航到一個鏈接的地址,你可以100%相信如果defaultPrevented是真的,用戶沒有去任何地方。
換句話說,這種技巧既比stopPropagation更可靠,而且不會有任何的副作用。
結論
希望本文能夠幫助你從一個新角度思考DOM事件。他們不是可以修改而不考慮后果的孤立的塊。他們是全局的,相互關聯的對象,常常影響到遠比你能意識到的更多的代碼。
為了避免錯誤,最好讓事件孤立,讓他們就像瀏覽器預期的一樣冒泡。
如果你不確定要做什么,問問自己以下問題:有沒有可能是現在或將來其他的一些代碼,可能想讓這事件發生?答案通常是肯定的。無論是對瑣事的Bootstrap模式或關鍵事件跟蹤分析,獲得事件對象是非常重要的。有疑問時,不要阻止冒泡。
avascript原生阻止冒泡
<html>
<head>
<style type="text/css">
#hide{width:75%;height:80px;background:skyblue;display:block;}
.hander{cursor:pointer;}
input{margin:5 0 0 900;}
</style>
<script>
//不用window.onload也可以
document.documentElement.onclick=function() {
document.getElementById('hide').style.display='none';
}
//阻止冒泡事件方法
function stopBubble(e) {
e=e || window.event;
if(e.stopPropagation) { //W3C阻止冒泡方法
e.stopPropagation();
} else {
e.cancelBubble=true; //IE阻止冒泡方法
}
}
//方法必須要放在window.onload下
window.onload=function(){
document.getElementById("hide").onclick=function(e) {
stopBubble(e);
}
document.getElementById('btn_show').onclick=function(e) {
document.getElementById('hide').style.display='block';
stopBubble(e);
}
}
</script>
</head>
<body>
<div id="hide" class="hander">click here nothing happen,you can click beside this area</div>
<input type="button" id="btn_show" value="show" class="hander"/>
</body>
onclick 如何阻止事件冒泡
<div id="id_tag_list"><span class="right">▼</span><span class="label" onclick="deleteLabel(this)">Python數據分析與應用</span></div>
//刪除標簽的方法
function deleteLabel(target){
var e=target;
if(e && e.stopPropagation){ //阻止冒泡
// 因此它支持W3C的stopPropation()方法
e.stopPropagation();
}else{
// 否則, 我們得使用IE的方法來取消事件冒泡
window.event.cancelBubble=true;
}
}
jQuery如何阻止事件冒泡
<div class="subcategories">這里有點擊事件</div>
<div class="subcategories">這里有點擊事件</div>
<div class="subcategories">這里有點擊事件</div>
<script type="text/javascript">
//阻止點擊事件
$(function() {
$(".subcategories").click(function(event) {
event.stopPropagation();
});
});
</script>
什么是js事件冒泡?
在一個對象上觸發某類事件(比如單擊onclick事件), 如果此對象定義了此事件的處理程序, 那么此事件就會調用這個處理程序, 如果沒有定義此事件處理程序或者事件返回true,
那么這個事件會向這個對象的父級對象傳播, 從里到外, 直至它被處理(父級對象所有同類事件都將被激活), 或者它到達了對象層次的最頂層, 即document對象(有些瀏覽器是window)。
如何來阻止Jquery事件冒泡?
<!DOCTYPE html>
<html">
<head runat="server">
<title>Jquery 事件冒泡</title>
<script src="jquery-1.3.2-vsdoc.js" type="text/javascript"></script>
</head>
<body>
<form id="form1" runat="server">
<div id="divOne" onclick="alert('我是最外層');">
<div id="divTwo" onclick="alert('我是中間層!')">
<a id="hr_three" href="http://www.xxxxxx.com" mce_href="http://www.xxxxxx.com"onclick="alert('我是最里層!')">點擊我</a>
</div>
</div>
</form>
</body>
</html>
比如上面這個頁面,
分為三層:divOne是第外層,divTwo中間層,hr_three是最里層;
他們都有各自的click事件,最里層a標簽還有href屬性。
運行頁面,點擊“點擊我”,會依次彈出:我是最里層---->我是中間層---->我是最外層
---->然后再鏈接到百度.
這就是事件冒泡, 本來我只點擊ID為hr_three的標簽, 但是確執行了三個alert操作。
事件冒泡過程(以標簽ID表示):hr_three----> divTwo----> divOne 。從最里層冒泡到最外層。
如何來阻止?
1.event.stopPropagation();
<script type="text/javascript">
$(function() {
$("#hr_three").click(function(event) {
event.stopPropagation();
});
});
<script>
再點擊"點擊我", 會彈出:我是最里層, 然后鏈接到百度
2.return false;
如果頭部加入的是以下代碼
<script type="text/javascript">
$(function() {
$("#hr_three").click(function(event) {
return false;
});
});
<script>
再點擊"點擊我", 會彈出:我是最里層, 但不會執行鏈接到百度頁面
由此可以看出:
1.event.stopPropagation();
事件處理過程中,阻止了事件冒泡,但不會阻擊默認行為(它就執行了超鏈接的跳轉)
2.return false;
事件處理過程中,阻止了事件冒泡,也阻止了默認行為(比如剛才它就沒有執行超鏈接的跳轉)
還有一種有冒泡有關的:
3.event.preventDefault();
如果把它放在頭部A標簽的click事件中, 點擊"點擊我"。
會發現它依次彈出:我是最里層---->我是中間層---->我是最外層, 但最后卻沒有跳轉到百度
它的作用是:事件處理過程中, 不阻擊事件冒泡, 但阻擊默認行為(它只執行所有彈框, 卻沒有執行超鏈接跳轉)
當調用的函數,有多個參數傳入時,需要使用原生DOM事件,則通過 $event 作為實參傳入
.enter
.tab
.delete (捕獲“刪除”和“退格”鍵)
.esc
.space
.up
.down
.left
.right
*請認真填寫需求信息,我們會在24小時內與您取得聯系。