JS中動態新增元素並繫結事件,造成程式重複執行

地鐵上的小前端發表於2019-03-04

那些莫名奇妙的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
版權所有,歡迎保留原文連結進行轉載:)

相關文章