removeEventListener “不生效”的思考

緣起

有時候,在項目中出現的問題,往往是因爲對一些基本概念理解的不太透徹,導致在使用過程中進行大量的誤用,最後導致在找 bug 的過程中搞得心力交瘁。

最近我就碰到一起由於 removeEventListener 移除“失效” 導致引起的 bug。

起因是由於,我們之前做了一個項目,項目是用 vue 寫的。由於是和同事一起寫的,所以對於有些地方,掌握的還是很難達到自己寫的代碼那麼透徹的。

因爲我們的項目需要做到頁面的自適應,因此我們會監聽 resize 事件,然後做一些自適應的調整。

但是某天,同時突然過來反映說,由於我之前在某個根組件,用了 v-if 的屬性,導致組件會被銷燬,但是銷燬的過程中,子組件中的監聽事件沒有被銷燬掉,導致會出現瘋狂報錯的情況。

一頓操作猛如虎

作爲一枚程序員,碰到問題的第一反映,當然是馬上進行調試,試圖找到問題所在。

於是,一頓測試,確實發現存在這個問題。

而且由於事件沒有被銷燬掉,如果窗口不斷的在兩種情況下相互切換,會導致監聽事件一直堆積在內存中,無法銷燬掉,極度影響項目的性能。

但是這個問題,很多時候其實是不影響項目的正常使用的,畢竟,正常使用的時候,沒有人會一直閒的無聊,去改變可可是區域窗口的大小的吧,

調試頁面的性能問題,也是有技巧的,有人說打斷點,或者用 console 啥的。

這些方式我都嘗試過,不過最近發現一個更直觀的工具,那就是 chrome devtool 提供的 preformance monitor 功能,這個可以實時的統計出來頁面的內存佔用,頁面上已經添加的監聽事件數量等等情況。

image.png

沒用過的朋友們,可以打開 chrome devtool 嘗試下。

細微之處見真知

但是本着打破砂鍋問到底的精神,我還是研究了一下爲什麼會出現這個問題。

碰到這個問題,我的第一反應是,是不是自己在寫代碼的過程中,由於疏忽,沒有在組件銷燬之前移除掉添加的監聽事件呢?

但是我通讀了一遍代碼以後,發現每次 beforeDestroy 的時候,我都有調用了 removeEventListener 去移除事件。

後面我又想,難道是,v-if 沒有在條件不成立的時候,銷燬掉組件麼?但是我馬上又否認了這個想法,如果 vue 有這麼大的 bug,我應該沒有機會成爲它的第一個發現者吧,畢竟 vue 是一個如此成熟、火爆的前端框架。

但是爲了保險起見,我還是用代碼調試了一下,結果發現,並沒有問題,組件確實是被銷燬掉了。

但是爲什麼事件沒有被銷燬掉呢?

難道是因爲我移除事件的方法有問題?

於是不甘認輸的我寫了個頁面進行測試了一下,還好,我以往的認知並沒有這麼快被打破,但是問題究竟出現在哪兒呢?

爲了模擬 vue 組件銷燬和添加的時候,事件添加和移除的邏輯,我寫了如下一個測試代碼:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>resize test</title>
</head>
<body>
  <button id="add" onclick="add()">添加事件</button>
  <button id="remove" onclick="remove()">移除事件</button>
  <script>
    let eventList = {
      0: () => {
        console.log(0);
      },
      1: () => {
        console.log(1);
      }
    };
    let count = 0;
    function add(e){
      window.addEventListener('resize', eventList[count]);
      count += 1;
    }

    function remove(e){
      count--;
      window.removeEventListener('resize', eventList[count]);
    } 
  </script>
</body>
</html>

邏輯很簡單,就是在頁面上加了兩個按鈕,一個用來添加事件,一個用來移除事件,爲了保險起見,我還添加了多個事件進行測試。

但是令人遺憾的是,測試的結果沒有絲毫問題,可以正常的添加也可以正常的移除掉,這讓我不禁產生了懷疑,到底是哪裏出現了問題。

後來我靈機一動,莫不是移除的事件和添加的事件不是同一個,導致在項目中移除不成功,而出現問題麼?

後來我再一檢查項目,果然就發現問題所在了,原來是同事在優化代碼的時候,把事件處理這塊,也一併優化掉了。

原本我們添加到 vue 組件的 methods 中的代碼,並不需要是手動綁定作用域,但是同事對這塊的掌握程度不夠,對監聽事件手動進行了 bind 操作。

想來也是慚愧,我之前檢查的時候,居然也沒有發現存在這個問題,所以才導致後面折騰了這麼久,才定位到問題所在。

不夠其實也是自己在這種問題上認識不深導致的。

雖然從寫 js 第一天開始,就知道自己手動添加的事件,必須要配套手動刪除的邏輯。

不然,很多時候的小疏忽,會釀成大問題。

但是在真正寫代碼的時候,其實是很難從頭到尾貫徹執行下去的。

瞭解到問題以後,我開始反思自己這種惰性思維。

我開始手動排查整個項目中的代碼,發現有的地方自己也沒有重視起來,也沒有寫上移除事件的邏輯!

不覺,大感慚愧。

追根溯源

而後我從 vue 官網找到的文檔中,找到了下面一段文字:

image.png
可以看到的是,文檔中很清楚的寫到了,methods 中的方法的 this 會自動綁定爲 Vue 實例。

至於爲什麼會這樣,其實很好理解。

因爲 Vue 的組件,其實更像是一段配置文件,一般情況下我們是無法更改配置文件的構造函數的,但是我們這地方寫的方法,必須要獲取到實例上的屬性和方法,不然,接下來一切都無法進行了。

所以我們在添加自己的監聽事件的時候,並不需要再次的 bind this,這樣會導致重新生成一個方法,所以在移除的時候,肯定是移除不掉了。

但是用過 react 的人肯定會很奇怪,爲什麼 react 中,我必須要 constructor 中手動 bind this 呢?那是因爲 vue 中自動爲我們做了這樣的操作,而 react 中,我們自己用類或者函數式的方式構造組件,我們必須要自己去執行這樣的綁定操作。

舉一反三

既然這個默認的監聽會存在組件銷燬未被移除的問題,那我在 vue 中用的 eventBus 會不會也出現這種問題呢?

我們在寫代碼的時候,特別是在寫 vue 的時候,會單獨構造一個事件總線(eventBus),去管理組件之間事件的傳遞。

一般是通過 eventBus.oneventBus.on 這個方式去添加監聽事件,通過 eventBus.emit 去派發事件的。

但是,我卻好像一直忘記寫把這個事件總線上的監聽給移除掉的邏輯了。

在項目裏一檢查,果然發現存在這個問題。

而 eventBus 這個事件調試起來就更簡單了。eventBus 是一個 js 對象而已,而添加的事件肯定是存儲在這個對象上的。

果然在 eventBus 對象上,存在 _events 這個屬性,這裏面就存儲着我們添加的各種事件。

組件銷燬的時候,還需要通過 $off 的方式,將監聽事件給移除掉,不然,這個事件一直會響應。

思考

我不禁在思考,很多人說,js 不是一門很好的語言,坑太多了,這點其實我是贊成的,但是有時候這些所謂的坑,只是我們對語言的理解程度不夠深造成的吧。

有時候,自由度高其實並不是缺點,最關鍵點在於使用者吧,

就像同樣一支筆,別人寫出來的字能稱之爲書法,而我們很多人寫的,頂多只能算作是字符而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章