那些莫名奇妙的bug
歇了兩週沒寫點什麼了,感覺最近有點知識慌,分享一下前段時間遇到的bug,這個Bug是關於jquery 的on方法綁互動事件,類似於$(`#point`).on(`click`,`.read-more`,function () {})這樣的程式碼造成的程式重複執行,很多人在文章裡寫到了,也說了用off方法來解綁,但都未能點出問題的本質,幾乎都忽略了問題的本質其實是事件委託造成的。話不多說,上點天天看到的程式碼:
第一種:
$(document).on(`click`, function (e) {
consol.log(`jquery事件繫結`)
});複製程式碼
第二種:
document.addEventListener(`click`,function (e) {
consol.log(`原生事件繫結`)
});複製程式碼
第三種:
var id = setInterval(function () {
console.log(`定時器迴圈事件繫結`)
},1000);複製程式碼
上面的程式碼,相信不少同盟,天天都會寫到,看似簡單的事件繫結,卻經常能給我們帶來意想不到的結果,特別是在這個SPA,應用AJAX頁面區域性重新整理如此盛行的時代。那什麼是事件繫結,造成的程式重複執行呢?這個事情要說清除,好像不是那麼簡單,還是用一段測試程式碼來說明吧。你可以拷貝到本地,自己試試:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button class="add_but">點選</button>
<div id="point">fdfsdf
</div>
<script src="https://cdn.bootcss.com/jquery/1.8.3/jquery.js"></script>
<script>
var count=1;
var example = {
getData:function () {
var data ={
content:`df`+count++,
href:``
};
this.renderData(data);
},
renderData:function (data) {
document.getElementById(`point`).innerHTML=`<div>this is a `+data.content+`點此<a class="read-more" href="javasript:;">檢視更多</a></div>`;
$(`#point`).on(`click`,`.read-more`,function () {
alert(`事故發生點`);
})
/* setInterval(function () {
console.log(`fdfdfg`);
},2000);*/
/*用冒泡來繫結事件,類似於Jquery的on繫結事件*/
/* document.querySelector(`body`).addEventListener(`click`,function (e) {
if(e.target.classList.contains(`read-more`)){
alert(`事故發生點`);
}
})*/
}
} ;
document.querySelector(`.add_but`).addEventListener(`click`,function (e) {
example.getData();
e.stopImmediatePropagation();
});
</script>
</body>
</html>複製程式碼
以上是我為說清這個事情寫的一段測試程式碼,可以拷貝下來試試。當我們點選頁面的按鈕,觸發呼叫example.getData()這個函式,模擬ajax獲取資料成功後,就會根據區域性重新整理頁面內元素類名為point的內容,同時會為載入這個內容中的read-more A標籤繫結一個事件,就這樣我們想要的效果出現啦,當元素第一次載入時,頁面正常,‘事故發生點’彈出一次,當二次重新整理觸發後,你會發現其彈出了兩次,當第三次時,你會發現,其彈三次,以此類推。。。。OMG,這個程式到底怎麼了,我明明每次事件繫結前,前面繫結的元素都刪除了,為什麼,被刪除的屍體感覺還在動作,好吧,上面就是我第一次遇到這個情況發出的感嘆。
最後是問身邊的大神,才突然領悟,原來繫結一直都在,而這個繫結被儲存在一個叫做事件佇列的地方,他不在迴圈執行的主執行緒中,畫了一張需要默契才能看懂的圖,勉強看一看。
還原真相
其實上面那一段程式碼是為了測試而特意寫的程式碼,除了定時器外,其他兩個點選事件換個正常的寫法,重複執行的情況是不會出現的,正常的程式碼:
// jquery 事件直接繫結的寫法;
$(`#point .read-more`).on(`click`,function () {
alert(`事故發生點`);
})
// 原生JS 事件直接繫結的寫法;
document.querySelector(`.read-more`).addEventListener(`click`,function (e) {
alert(`事故發生點`);
})複製程式碼
看出差別了嗎?其實就是不用冒泡來事件委託,而是直接給新增的元素繫結事件。所以Dom事件是講道理的,動態新增的元素,再動態為此繫結事件,待元素被刪除後,與其繫結的相應事件其實是會從事件繫結佇列中刪除的,而非如上面測試程式碼,給人的感覺是元素移除後,但其繫結的事件還在記憶體中。但請記住,這是個誤會,上面測試的程式碼之所以給人這種錯覺,是因為我們並沒有為動態新增的元素繫結事件,而僅僅是用了事件委託的形式,實際上事件是繫結在#point元素上的,其一直存在,利用事件冒泡來讓程式知道我們點選了動態新增的連結元素。測試中特意用原生js去重現了這次事件委託,jquery的on繫結事件其實原理基本相同。
document.querySelector(`body`).addEventListener(`click`,function (e) {
if(e.target.classList.contains(`read-more`)){
alert(`事故發生點`);
}
})複製程式碼
解除bug的那些方法
定時器
這個是最易犯的錯誤,當然也是最易解的錯誤,因為設定定時器時,其會返回一個數值,這個數值應該是事件佇列此定時器中的一個編號吧,類似於9527;步驟就是設定一個全域性變數來保持這個返回值id,在每次設定定時器時,先通過id清除已經設定過的定時器
clearInterval(intervalId); //粗暴的寫法
intervalId&&clearInterval(intervalId); //嚴謹的寫法
intervalId=setInterval(function () {
console.log(`fdfdfg`);
},2000); 複製程式碼
Dom事件
其實上面我們已經說過,最直接的辦法就是不採用事件委託,而是採用直接繫結;如果確實要用事件委託來繫結事件,那就是解綁。在jquery中提供了unbind函式來解綁事件,不過在jquery 1.8版本以後,這個方法已經不推薦了,而是推薦off方法。比如上面的on事件委託的方式,要解綁,可採用語句$(`#point`).off(`click`,`.read-more`)。
有缺陷的解決方案,新增flag
很好理解,第一次繫結後,flag置位,下一次在執行這個繫結時,程式就知道在這個節點上已經有了繫結,無需再新增,具體操作就是:
var flag = false;
var example = {
getData: function () {
var data = {
content: `df` + count++,
href: ``
};
this.renderData(data);
},
renderData: function (data) {
document.getElementById(`point`).innerHTML = `<div>this is a ` + data.content + `點此<a class="read-more" href="javasript:;">檢視更多</a></div>`;
!flag && $(`#point`).on(`click`, `.read-more`, function () {
alert(`事故發生點`+data.content);
});
flag = true;
}
};複製程式碼
從邏輯上,看起來沒有問題,但仔細觀察,發現這是有問題的。當我們第二次,第三次重新整理時,彈出框的內容還是和第一次模擬重新整理後點選後彈出的內容一致,還是`事故發生點df1`,而非和內容一樣遞增,為什麼呢,感覺事件佇列裡面的回撥函式被單獨儲存起來了,data被深拷貝了,而不再是一個引用。確實有點難理解,我也不知道到底是為什麼,如果哪位能說清楚,還請一定告知。
結個尾
寫在最後,其實平常寫一些程式時,事件繫結,造成程式重複執行這些情況很少發生,其通常會出現在我們寫外掛的時候,外掛需要適應多種呼叫環境,所以在外掛內部做到防止事件重複繫結的情況非常重要。
本文首發於:closertb.site
版權所有,歡迎保留原文連結進行轉載:)