0.起因
前幾天寫業(yè)務(wù)的時候,碰到了這樣的需求:
checkbox在父DIV的里面,但是checkbox綁定了v-model,要求點擊圖片的時候讓checkbox狀態(tài)改變.
這個需求說起來挺簡單的,綁定之后寫一個方法,讓checkbox的值跟著走就行了.但是我碰到了幾個小問題.解決不難,但是我希望能夠進行輸出并追根問底,形成自己的知識體系.
那么,開始吧.
1.修改狀態(tài)帶來的問題.
input中checkbox的代碼簡化如下:
<input type="checkbox" v-model="pro.IsSelect" @click.stop="test(pro)">
已知這個checkbox在一個大的div內(nèi),但是現(xiàn)在的問題是,大的DIV上綁定了一個這樣的事件:
prochange:function(data){
data.IsSelect=!data.IsSelect;
},
可以看到,我使用這一個事件進行checkbox狀態(tài)的修改
這樣會產(chǎn)生一個問題,那就是我在點擊大DIV的時候會正常觸發(fā)事件,但是點擊小的checkbox則不會.
解決方法很簡單,在小事件上加上阻止事件冒泡即可.
.....@click.stop="test(pro)">
這樣就解決了.但是我心中的疑惑還是沒有消除.冒泡究竟是怎么樣的?我粗淺的知識應(yīng)付工作是足夠了,但是我真的了解冒泡嗎?
2.冒泡和捕獲,關(guān)于事件監(jiān)聽
解決問題的第一個方法當然是找資料.我之前進行過捕獲冒泡相關(guān)知識的學(xué)習(xí),但是印象并不是很深刻,我覺得我有必要重新總結(jié)一下.
2.1鼠標點擊事件的區(qū)別
在我鼠標左鍵點擊網(wǎng)頁的這一刻,究竟發(fā)生了什么?我想大概是:
- 系統(tǒng)級API捕捉到了我的鼠標的動作
- 系統(tǒng)將這個事件傳遞給瀏覽器.
- 瀏覽器通過DOMAPI通知網(wǎng)頁.
- 網(wǎng)頁進行事件對應(yīng)的操作.
2.2原始的方法-事件綁定
在DOM Level2(鏈接至我認為將DOM level說的比較清楚的文章)之前,其實能夠使用的方法只有事件綁定.
沒錯,就是古老的xxx.onclck方法.
而DOMlevel2的介紹是這樣的:
DOM2級引入了下列新模塊樊诺,也給出了眾多新類型和新接口的定義化焕。
DOM視圖(DOM Views):定義了跟蹤不同文檔(例如,應(yīng)用CSS之前和之后的文檔)視圖的接口愧沟;
DOM事件(DOM Events):定義了事件和事件處理的接口;
DOM樣式(DOM Style):定義了基于CSS為元素應(yīng)用樣式的接口芬为;
DOM遍歷和范圍(DOM Traversal and Range):定義了遍歷和操作文檔樹的接口模闲。
在DOM2添加事件監(jiān)聽之后,我們開始面對著一個新的問題.
2.3child元素和parent元素的通知問題:冒泡和捕獲
要說捕獲和冒泡,得先介紹事件監(jiān)聽忘嫉。
事件監(jiān)聽和綁定其實差不了太多荤牍,但是新增了一個特點:就是無論監(jiān)聽多少次,都不會覆蓋掉前面的事件庆冕。
捕獲和冒泡其實本質(zhì)上只是Child和Parent通知的兩種順序康吵。
捕獲指的是parent先通知,child后通知访递,而冒泡則相反.
比如,就拿業(yè)務(wù)上碰到的這個小問題作為例子.已知Chrome使用的是事件冒泡,那么通知順序應(yīng)該是:
點擊checkbox-通知child(checkbox)值改變-通知parent-觸發(fā)parent綁定的事件-parent上綁定的!IsSelect觸發(fā)-checkbox不變
因為我們在parent上綁定了事件,所以現(xiàn)象是這樣的:
可以看到這個bug的復(fù)現(xiàn).
那么我們說過,要解決這個問題,需要阻止它的事件冒泡.也就是,點擊checkbox不觸發(fā)綁定在parent上的事件,在Vue中的語法糖是@click.stop.
而這個stop實際上便是源生中的e.stopPropagation().
那么捕獲呢?
捕獲的默認效果其實在早期的瀏覽器中比較常見,但是現(xiàn)在依然可以實現(xiàn)用捕獲來代替冒泡.在源生方法監(jiān)聽事件的addEventListener中,我們可以看到API的三個參數(shù)分別是:
target.addEventListener(type, listener[, options]);
我們可以使用第三個參數(shù)來控制冒泡或捕獲.
false:以冒泡方式執(zhí)行事件監(jiān)聽.true:以捕獲方式進行事件監(jiān)聽.
3.事件委托(事件代理)
介紹完上面的晦嵌,事件委托是時候登場了。事件委托簡單說起來就是利用事件冒泡,只指定一個事件處理程序惭载,就可以管理某一類型的所有事件旱函。
網(wǎng)上我找了很多篇博客,基本都是用這個例子描滔,我就直接抄過來了:
有三個同事預(yù)計會在周一收到快遞棒妨。為簽收快遞,有兩種辦法:一是三個人在公司門口等快遞含长;二是委托給前臺MM代為簽收∪唬現(xiàn)實當中,我們大都采用委托的方案拘泞,前臺MM收到快遞后纷纫,她會判斷收件人是誰,然后按照收件人的要求簽收陪腌,甚至代為付款涛酗。這種方案還有一個優(yōu)勢,那就是即使公司里來了新員工(不管多少)偷厦,前臺MM也會在收到寄給新員工的快遞后核實并代為簽收商叹。
看著是不是有點...眼熟?這概念是不是很像工廠模式?
用一個實例來進行說明吧.
<body>
<ul id="ul1">
<li>A quick brown</li>
<li>fox jump over</li>
<li>the</li>
<li>lazy dog</li>
</ul>
</body>
<script>
window.onload = function(){
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(123);
}
}
}
</script>
3-1改進導(dǎo)致的新bug
這是一段標準的源生JS遍歷li,效果是每次點擊li則alert出一個123.但是這么寫明顯很蠢,因為我們監(jiān)聽ul就可以達到同樣的效果.
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(){
alert(123);
}
嗯,這樣就實現(xiàn)了功能.但是此時出現(xiàn)了一個bug.
當我們點擊的區(qū)域/元素在每個li之外,但是在ul之內(nèi)時,一樣會觸發(fā)事件.
嗯,這樣很討厭.我們想要的是,點擊li才會觸發(fā).
這里有一種Hack方法可以解決,那就是改變ul的樣式,使得ul不會被點擊到,這里就不細說了,說正道.
3-2簡單的處理辦法
正道走下去,會發(fā)現(xiàn)我們需要加一個判斷.寫出來大概是這樣的:
ul.addEventListener('click', function(e) {
// 檢查事件源e.targe是否為Li
if (e.target && e.target.nodeName.toUpperCase == "LI") {
// 真正的處理過程在這里
console.log("123");
}
}
加一個簡單的判斷就可以達到我們的目標效果.我能在網(wǎng)上搜索到的博客也基本都是這么做的.
但是這么做真的沒有問題嗎?我很好奇.
如果,點擊到的不是li呢?
<ul id="ul1">
<span><li>111</li></span>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
將HTML改成這樣之后,我們發(fā)現(xiàn),監(jiān)聽失效了,點擊li不會觸發(fā)alert.實際原因是,點擊到的根本不是li,而是span.也就是說,上一段代碼并不優(yōu)雅,它忽略了HTML可能修改的可能性.略微思索之后,我們把JS改成了這樣的:
ul.addEventListener('click', function() {
let el = e.target
while (el && !el.matches(selector)) {
el = el.parentNode
if (element === el) {
el = null
}
}
if (el) {
console.log('123')
}
}
這樣的代碼,才能保證基本的健壯性.
寫到這里,我已經(jīng)沒有任何疑問了.
4.極端情況的探尋
剛準備寫完收工,突然想到了一個讓我有點糾結(jié)的問題,決定親手試一試.
如果同時綁定了捕獲和冒泡,那么執(zhí)行順序?qū)鞘裁礃拥?
<body>
<div id='one'>
<div id='two'>
<div id='three'>
<div id='four'>
</div>
</div>
</div>
</div>
<script type='text/javascript'>
var one=document.getElementById('one');
var two=document.getElementById('two');
var three=document.getElementById('three');
var four=document.getElementById('four');
one.addEventListener('click',function(){
alert('one');
},false);
two.addEventListener('click',function(){
alert('two');
},false);
three.addEventListener('click',function(){
alert('three');
},false);
four.addEventListener('click',function(){
alert('four');
},false);
</script>
搜了一下,發(fā)現(xiàn)很多人和我有相同的疑慮,找了他們的代碼來手動測試一下.首先是全冒泡的結(jié)果:
可以看到,結(jié)果是one-two/one-three/two/one-four-three-two-one.可以看到,瀏覽器的表現(xiàn)與我們的知識沒有出入.
看到這里,我們可以略過全是捕獲的測試了,能夠預(yù)想到結(jié)果是啥了.
但是,如果冒泡和捕獲同時存在呢?
我們把測試函數(shù)改成這樣:
<script type='text/javascript'>
var one=document.getElementById('one');
var two=document.getElementById('two');
var three=document.getElementById('three');
var four=document.getElementById('four');
one.addEventListener('click',function(){
console.log('one');
},false);
two.addEventListener('click',function(){
console.log('two');
},true);
three.addEventListener('click',function(){
console.log('three');
},false);
four.addEventListener('click',function(){
console.log('four');
},true);
這樣會輸出怎樣的結(jié)果呢?說實話,我并不知道.所以我決定試試.
這個結(jié)果就有點意思了.點擊two的時候,因為two是捕獲,那么應(yīng)該顯示one-two,但是實際上顯示的是two-one.點擊three的時候,顯示的是two-three-one,點擊four顯示的是two-four-three-one那么這種混雜的情況是怎么運行的呢?
在進行了幾次實驗后,我得出了這個結(jié)論.
在冒泡與捕獲同時存在時,會先執(zhí)行從外到內(nèi)的捕獲,然后再執(zhí)行從內(nèi)到外的冒泡
所以,two-four-three-one應(yīng)該這么解釋:
當點擊four時,優(yōu)先執(zhí)行父級元素捕獲-執(zhí)行two的捕獲-執(zhí)行four的捕獲-執(zhí)行three的冒泡-執(zhí)行one的冒泡.
今天花了很多時間總結(jié)了這個知識,但是并不覺得虧.以后再碰到這種坑,應(yīng)該是再也不會掉進去了.