vue使用中的記憶體洩漏

mushang發表於2018-06-26

今天看到一篇關於js使用中記憶體洩露的文章,以及chrom瀏覽器檢視記憶體洩漏的方法,決定留著。本文只擷取了我認為比較重要的部分,喜歡原文的小夥伴,請點選文章下方的原文連結。

什麼是記憶體洩露?記憶體洩露是指new了一塊記憶體,但無法被釋放或者被垃圾回收。new了一個物件之後,它申請佔用了一塊堆記憶體,當把這個物件指標置為null時或者離開作用域導致被銷燬,那麼這塊記憶體沒有人引用它了在JS裡面就會被自動垃圾回收。但是如果這個物件指標沒有被置為null,且程式碼裡面沒辦法再獲取到這個物件指標了,就會導致無法釋放掉它指向的記憶體,也就是說發生了記憶體洩露。為什麼程式碼裡面會拿不到這個物件指標了呢,舉一個例子:

// module date.js
let date = null;
export default {
    init () {
        date = new Date();
    }
}
// main.js
import date from 'date.js';
date.init();複製程式碼

在main.js初始化了date之後,date這個變數就一會直存在了,直到你把頁面關了,因為date的引用是在另一個module裡面,可以理解為模組就是一個閉包對外是不可見的。所以如果你是希望這個date物件一直存在、需要一直使用的話,那麼沒有問題,但是如果想用一次就不用了那就會有問題,這個物件一直在記憶體裡面沒有被釋放就發生了記憶體洩露。

另一種比較隱蔽並且很常見的記憶體洩露是事件繫結,形成了一個閉包,導致一些變數一直存在。如下例子所示:

// 一個圖片懶惰載入引擎示例
class ImageLazyLoader {
    constructor ($photoList) {
        $(window).on('scroll', () => {
            this.showImage($photoList);
        });
    }
    showImage ($photoList) {
        $photoList.each(img => {
            // 通過位置判斷圖片滑出來了就載入
            img.src = $(img).attr('data-src');
        });
    }
}
// 點選分頁的時候就初始化一個圖片懶惰載入的
$('.page').on('click', function () {
    new ImageLazyLoader($('img.photo'));
});複製程式碼

這是一個圖片懶惰載入的模型,每次點分頁的時候就會清掉上一頁的資料更新為當前頁的DOM,並重新初始化一個懶惰載入的引擎。它裡面監聽了scroll事件,對傳進來的圖片列表的DOM進行處理。每點一次分頁就會重新new一個,這裡就發生了記憶體洩露,主要是以下3行程式碼導致的:

$(window).on('scroll', () => {
    this.showImage($photoList);
});複製程式碼

因為這裡的事件繫結形成了一個閉包,this/$photoList這兩個變數一直沒有被釋放,this是指向ImageLazyLoader的例項,而$photoList是指向DOM結點,當清除掉上一頁的資料的時候,相關DOM結點已經從DOM樹分離出來了,但是仍然還有一個$photoList指向它們,導致這些DOM結點無法被垃圾回收一直在記憶體裡面,就發生了記憶體洩露。由於this變數也被閉包困住了沒有被釋放,所以還有一個ImageLazyLoader的例項發生記憶體洩露。

這個的解決方法比較簡單,就是銷燬例項的時候把繫結的事件off掉,如下程式碼所示:

class ImageLazyLoader {
    constructor ($photoList) {
        this.scrollShow = () => {
            this.showImage($photoList);
        };
        $(window).on('scroll', this.scrollShow);
    }
    // 新增一個事件解綁                           
    clear () {                     
        $(window).off('scroll', this.scrollShow);
    }
    showImage ($photoList) {
        $photoList.each(img => {
            // 通過位置判斷圖片滑出來了就載入
            img.src = $(img).attr('data-src');
        });
        // 判斷如果圖片已全部顯示,就把事件解綁了
        if (this.allShown) {
            this.clear();
        }
    }
}
// 點選分頁的時候就初始化一個圖片懶惰載入的
let lazyLoader = null;
$('.page').on('click', function () {
    lazyLoader && (lazyLoader.clear());
    lazyLoader = new ImageLazyLoader($('img.photo'));
});複製程式碼

在每次例項化一個ImageLazyLoader之前把先把上一個例項clear掉,clear裡面進行解綁,由於JS有建構函式但是沒有解構函式,所以需要自己寫一個clear,在外面手動調一下clear。同時在事件的執行過程的合適時機自動把事件給解綁了,上面是判斷如果所有的圖片都展示出來了那麼就沒必要監聽scroll事件了直接解綁了。這樣就能解決記憶體洩露的問題了,能夠觸發自動垃圾回收。

為什麼把事件解綁了,就不會有閉包引用了呢?因為JS引擎檢測到那個閉包沒用了,就把那個閉包銷燬了,那麼閉包引用的外部變數也自然會被置空。

好了,基礎知識就講解到這裡,現在用Chrome devtools的記憶體檢測工具來實際操作一遍,方便發現頁面的一些記憶體洩露行為。為了避免裝給瀏覽器裝的一些外掛造成影響,使用Chome的隱身模式頁面,它會把所有的外掛都給禁掉。

然後開啟devtools,切到Memory的tab,選中Heap snapshot,如下所示:

vue使用中的記憶體洩漏


什麼叫heap snapshot呢?翻譯一下就是堆快照,給當前記憶體堆拍一張照片。因為動態申請的記憶體都是在堆裡面的,而區域性變數是在記憶體棧裡面,是由作業系統分配管理的是不會記憶體洩露了。所以關心堆的情況就好了。

然後做一些增刪改DOM的操作,如:

(1)彈一個框,然後把彈框給關了

(2)單頁面的點選跳轉到另一個路由,然後再點後退返回

(3)點選分頁觸發動態改DOM

就是先增加DOM,然後把這些DOM給刪了,看一下這些被刪除的DOM是否還有物件引用它們。

這裡我是第2種方式的場景,檢測單頁面應用的某個路由頁面是否存在記憶體洩露。先開啟首頁,點到另一個頁面,再點後退,接著點一下垃圾回收的按鈕:

vue使用中的記憶體洩漏

觸發垃圾回收,避免一些不必要的干擾。

然後再點一下拍照按鈕:

vue使用中的記憶體洩漏

它就會把當前頁面的記憶體堆掃描一遍顯示出來,如下圖所示:

vue使用中的記憶體洩漏

然後在上面中間的Class Filter的搜尋框裡搜一下detached:

vue使用中的記憶體洩漏

它就會顯示所有已經分離了DOM樹的DOM結點,重點關注distance值不為空的,這個distance表示距離DOM根結點的距離。上圖展示的這些div具體是啥呢?我們把滑鼠放上去不動等個2s,它就會顯示這個div的DOM資訊:

vue使用中的記憶體洩漏

通過className等資訊可以知道它就是那個要檢查的頁面的DOM節點,在下面的Object的視窗裡面依次展開它的父結點,可以看到它最外面的父結點是一個VueComponent例項:

vue使用中的記憶體洩漏

下面黃色字型native_bind表示有個事件指向了它,黃色表示引用仍然生效,把滑鼠放到native_bind上面停留2秒:

vue使用中的記憶體洩漏


它會提示你是在homework-web.vue這個檔案有一個getScale函式繫結在了window上面,檢視一下這個檔案確實是有一個繫結:

mounted () {
    window.addEventListener('resize', this.getScale);
}複製程式碼

所以雖然Vue元件把DOM刪除了,但是還有個引用存在,導致元件例項沒有被釋放,元件裡面又有一個$el指向DOM,所以DOM也沒有被釋放。

要在beforeDestroyed裡面解綁的

beforeDestroyed () {
    window.removeEventListener('resize', this.getScale);
}複製程式碼

所以綜合上面的分析,造成記憶體洩露的可能會有以下幾種情況:

(1)監聽在window/body等事件沒有解綁

(2)綁在EventBus的事件沒有解綁

(3)Vuex的$store watch了之後沒有unwatch

(4)模組形成的閉包內部變數使用完後沒有置成null

(5)使用第三方庫建立,沒有呼叫正確的銷燬函式

並且可以藉助Chrome的記憶體分析工具進行快速排查,本文主要是用到了記憶體堆快照的基本功能,讀者可以嘗試分析自己的頁面是否存在記憶體洩漏,方法是做一些操作如彈個框然後關了,拍一張堆快照,搜尋detached,按distance排序,把非空的節點展開父級,找到標黃的字樣說明,那些就是存在沒有釋放的引用。也就是說這個方法主要是分析仍然存在引用的遊離DOM節點。因為頁面的記憶體洩露通常是和DOM相關的,普通的JS變數由於有垃圾回收所以一般不會有問題,除非使用閉包把變數困住了用完了又沒有置空。

DOM相關的記憶體洩露通常也是因為閉包和事件繫結引起的。綁了(全域性)事件之後,在不需要的時候需要把它解綁。當然直接綁在div上面的可以直接把div刪了,綁在它上面的事件就自然解綁了。

原文地址



相關文章