欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一個Vue頁面的內(nèi)存泄露分析詳解

 更新時間:2018年06月25日 09:07:36   作者:會編程的銀豬  
這篇文章主要介紹了一個Vue頁面的內(nèi)存泄露分析詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

什么是內(nèi)存泄露?內(nèi)存泄露是指new了一塊內(nèi)存,但無法被釋放或者被垃圾回收。new了一個對象之后,它申請占用了一塊堆內(nèi)存,當把這個對象指針置為null時或者離開作用域?qū)е卤讳N毀,那么這塊內(nèi)存沒有人引用它了在JS里面就會被自動垃圾回收。但是如果這個對象指針沒有被置為null,且代碼里面沒辦法再獲取到這個對象指針了,就會導致無法釋放掉它指向的內(nèi)存,也就是說發(fā)生了內(nèi)存泄露。為什么代碼里面會拿不到這個對象指針了呢,舉一個例子:

// 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這個變量就一會直存在了,直到你把頁面關(guān)了,因為date的引用是在另一個module里面,可以理解為模塊就是一個閉包對外是不可見的。所以如果你是希望這個date對象一直存在、需要一直使用的話,那么沒有問題,但是如果想用一次就不用了那就會有問題,這個對象一直在內(nèi)存里面沒有被釋放就發(fā)生了內(nèi)存泄露。

另一種比較隱蔽并且很常見的內(nèi)存泄露是事件綁定,形成了一個閉包,導致一些變量一直存在。如下例子所示:

// 一個圖片懶惰加載引擎示例
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'));
});

這是一個圖片懶惰加載的模型,每次點分頁的時候就會清掉上一頁的數(shù)據(jù)更新為當前頁的DOM,并重新初始化一個懶惰加載的引擎。它里面監(jiān)聽了scroll事件,對傳進來的圖片列表的DOM進行處理。每點一次分頁就會重新new一個,這里就發(fā)生了內(nèi)存泄露,主要是以下3行代碼導致的:

$(window).on('scroll', () => {
 this.showImage($photoList);
});

因為這里的事件綁定形成了一個閉包,this/$photoList這兩個變量一直沒有被釋放,this是指向ImageLazyLoader的實例,而$photoList是指向DOM結(jié)點,當清除掉上一頁的數(shù)據(jù)的時候,相關(guān)DOM結(jié)點已經(jīng)從DOM樹分離出來了,但是仍然還有一個$photoList指向它們,導致這些DOM結(jié)點無法被垃圾回收一直在內(nèi)存里面,就發(fā)生了內(nèi)存泄露。由于this變量也被閉包困住了沒有被釋放,所以還有一個ImageLazyLoader的實例發(fā)生內(nèi)存泄露。

這個的解決方法比較簡單,就是銷毀實例的時候把綁定的事件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有構(gòu)造函數(shù)但是沒有解構(gòu)函數(shù),所以需要自己寫一個clear,在外面手動調(diào)一下clear。同時在事件的執(zhí)行過程的合適時機自動把事件給解綁了,上面是判斷如果所有的圖片都展示出來了那么就沒必要監(jiān)聽scroll事件了直接解綁了。這樣就能解決內(nèi)存泄露的問題了,能夠觸發(fā)自動垃圾回收。

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

好了,基礎(chǔ)知識就講解到這里,現(xiàn)在用Chrome devtools的內(nèi)存檢測工具來實際操作一遍,方便發(fā)現(xiàn)頁面的一些內(nèi)存泄露行為。為了避免裝給瀏覽器裝的一些插件造成影響,使用Chome的隱身模式頁面,它會把所有的插件都給禁掉。

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

什么叫heap snapshot呢?翻譯一下就是堆快照,給當前內(nèi)存堆拍一張照片。因為動態(tài)申請的內(nèi)存都是在堆里面的,而局部變量是在內(nèi)存棧里面,是由操作系統(tǒng)分配管理的是不會內(nèi)存泄露了。所以關(guān)心堆的情況就好了。

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

(1)彈一個框,然后把彈框給關(guān)了

(2)單頁面的點擊跳轉(zhuǎn)到另一個路由,然后再點后退返回

(3)點擊分頁觸發(fā)動態(tài)改DOM

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

這里我是第2種方式的場景,檢測單頁面應(yīng)用的某個路由頁面是否存在內(nèi)存泄露。先打開首頁,點到另一個頁面,再點后退,接著點一下垃圾回收的按鈕:

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

然后再點一下拍照按鈕:

它就會把當前頁面的內(nèi)存堆掃描一遍顯示出來,如下圖所示:

然后在上面中間的Class Filter的搜索框里搜一下detached:

它就會顯示所有已經(jīng)分離了DOM樹的DOM結(jié)點,重點關(guān)注distance值不為空的,這個distance表示距離DOM根結(jié)點的距離。上圖展示的這些div具體是啥呢?我們把鼠標放上去不動等個2s,它就會顯示這個div的DOM信息:

通過className等信息可以知道它就是那個要檢查的頁面的DOM節(jié)點,在下面的Object的窗口里面依次展開它的父結(jié)點,可以看到它最外面的父結(jié)點是一個VueComponent實例:

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

它會提示你是在homework-web.vue這個文件有一個getScale函數(shù)綁定在了window上面,查看一下這個文件確實是有一個綁定:

mounted () {
 window.addEventListener('resize', this.getScale);
}

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

但是看代碼的話是在beforeDestroyed里面解綁的:

beforeDestroyed () {
 window.removeEventListener('resize', this.getScale);
}

所以應(yīng)該沒有問題啊?

定睛一看,傻眼了,原來函數(shù)名寫錯了,應(yīng)該是:

beforeDestroy () {
 window.removeEventListener('resize', this.getScale);
},

發(fā)現(xiàn)了一個隱藏多日的bug,因為這個比較隱蔽,就算寫錯了也不會有明顯的感知了。

把這個地方改一下,重復操作一遍,再拍一張內(nèi)存快照。我們發(fā)現(xiàn)游離的div節(jié)點仍然是74個且disance不為空,沒有改進如下圖所示:

難道剛剛改得不對?繼續(xù)查看剛剛第2個節(jié)點:

可以發(fā)現(xiàn),這次是有一個 事件總線EventBus的事件綁定指向了它 ,說明除了剛剛那個resize事件綁定之外,還有一個EventBus的事件沒有釋放,事件名稱是gToNextHomworkTask。我們搜一下這個事件是在哪里綁的,可以找到它是在路由組件的一個子組件里面綁的:

mounted () {
 EventBus.$on('goToNextHomeworkTask', this.go2NextQuestion);
}

果不其然,這個組件只有$on,沒有$off,所以導致組件卸載的時候仍然有一個事件的引用。所以需要在這個組件的destroyed里面給$off掉:

mounted () {
 EventBus.$off('goToNextHomeworkTask', this.go2NextQuestion);
}

改完后刷新頁面操作第3次,再拍一張內(nèi)存快照,比較尷尬的是情況還是一樣:

說明還有人引用它,繼續(xù)查看是誰引用了沒有釋放:

可以發(fā)現(xiàn)是一個 Vuex的$store的watch監(jiān)聽沒有釋放 ,借助Watcher的cb屬性可以知道具體是哪個監(jiān)聽函數(shù)。利用簡單的文本搜索發(fā)現(xiàn)是在一個子組件里面進行了watch:

mounted () {
 this.$store.watch(state => state.currentIndex, (newIndex, oldIndex) => {
  if (this.$refs.animation && newIndex === this.task.index - 1) {
   this.$refs.animation.beginElement();
  } 
 }); 
}

watch里面有一個this指針指向了組件的DOM元素,由于子組件沒有被釋放,那么包含它的父組件自然不會被釋放,所以一層層往上,導致最外面那個路由組件也不會被釋放。

這個需要在destroyed的時候unwatch一下:

mounted () {
 this.unwatchStore = this.$store.watch(state => state.currentIndex, (newIndex, oldIndex) => {
  // 代碼略
 }); 
},
destroyed () {
 this.unwatchStore();
}

處理完之后再拍一張內(nèi)存快照,如下圖所示:

雖然還是74個但是distance已經(jīng)為空了,可對比前3步distance都不為空,并且下面Object展開沒有找到標黃的部分了,也就是說這個路由組件內(nèi)存泄露的問題已經(jīng)得到解決。

我們繼續(xù)查看其它distance不為空的div節(jié)點,如下圖所示,可以按照distance排下序:

其中有一個是.animate-container:

它是一個用來放lottie動畫的DOM容器,lottie對象里面仍有引用它:

這個是一個用lottie做的loading動畫,當loading結(jié)束的時候,我會手動調(diào)一下它的stop api停止動畫,并且把.animte-container給remove掉,但是為什么lottie還不肯放過它呢?我的代碼是這么寫的:

let loadingAnimate = null;
let bodymovinAnimate = {
 // 顯示loading動畫
 showLoading () {
  loadingAnimate = bodymovinAnimate._showAnimate();
  return loadingAnimate;
 },
 // 停止loading動畫
 stopLoading () {
  loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate);
 },
 // 開始lottie動畫
 _showAnimate () {
  const animate = lottie.loadAnimation({
   // 參數(shù)省略
  }); 
  return animate;
 }
 // 結(jié)束lottie動畫
 _stopAnimate (animate) {
  animate.stop();
  let $container = $(animate.wrapper).closest('.bodymovin-container');
  $container.remove();
 },
};
export default bodymovinAnimate;

我猜想是調(diào)了stop之后lottie仍然沒有釋放對DOM的引用,因為stop之后還能夠夠支持重新start的,所以它得咬著DOM不放,因此如果要徹底結(jié)束動畫,應(yīng)該不是調(diào)stop,查了一下它還有一個destroy的方法,把stop換成destroy:

 // 結(jié)束lottie動畫
 _stopAnimate (animate) {
  animate.destroy();
  let $container = $(animate.wrapper).closest('.bodymovin-container');
  $container.remove();
 },

這樣改了之后,lottie的引用就會把它給釋放了,問題解決了,然后再重新拍一張照片:

仍然有一個exports.default指向它,它是webpack的模塊,我猜想是因為本文開篇提到的例子的原因,就是模塊形成了閉包,它的變量沒有被釋放造成內(nèi)存泄露,所以在stopLoading里面把它置成null:

 // 停止loading動畫
 stopLoading () {
  loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate);
  loadingAnimate = null;
 },

這樣試了之后,.animate-container這個DOM對象就沒有人引用它了。

最后div還剩下3個有distance:

其中兩個是jq的$.support.boxSizingReliable,是jq用來檢測boxszing是否可用創(chuàng)建的div:

還有一個是Vue的:

這些都是使用的庫造成的內(nèi)存泄露,暫時先不管。

再去分析其它的標簽也有類似的情況。

所以綜合上面的分析,造成內(nèi)存泄露的可能會有以下幾種情況:

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

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

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

(4)模塊形成的閉包內(nèi)部變量使用完后沒有置成null

(5)使用第三方庫創(chuàng)建,沒有調(diào)用正確的銷毀函數(shù)

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

DOM相關(guān)的內(nèi)存泄露通常也是因為閉包和事件綁定引起的。綁了(全局)事件之后,在不需要的時候需要把它解綁。當然直接綁在div上面的可以直接把div刪了,綁在它上面的事件就自然解綁了。

以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • vuejs實現(xiàn)下拉框菜單選擇

    vuejs實現(xiàn)下拉框菜單選擇

    這篇文章主要為大家詳細介紹了vuejs實現(xiàn)下拉框菜單選擇,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-10-10
  • VUE實現(xiàn)token登錄驗證

    VUE實現(xiàn)token登錄驗證

    這篇文章主要為大家介紹了VUE實現(xiàn)token登錄驗證,詳細記錄實現(xiàn)token登錄驗證的步驟,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • Vue3 Ref獲取真實DOM學習實戰(zhàn)

    Vue3 Ref獲取真實DOM學習實戰(zhàn)

    這篇文章主要為大家介紹了Vue3 Ref獲取真實DOM學習實戰(zhàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-06-06
  • vue mint-ui學習筆記之picker的使用

    vue mint-ui學習筆記之picker的使用

    本篇文章主要介紹了vue mint-ui學習筆記之picker的使用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-10-10
  • Vue-cli3.x + axios 跨域方案踩坑指北

    Vue-cli3.x + axios 跨域方案踩坑指北

    這篇文章主要介紹了Vue-cli3.x + axios 跨域方案踩坑指北,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-07-07
  • vuex學習之Actions的用法詳解

    vuex學習之Actions的用法詳解

    本篇文章主要介紹了vuex學習之Actions的用法詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • ElementUI時間選擇器限制選擇范圍disabledData的使用

    ElementUI時間選擇器限制選擇范圍disabledData的使用

    本文主要介紹了ElementUI時間選擇器限制選擇范圍disabledData的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-06-06
  • vue路由懶加載工作原理

    vue路由懶加載工作原理

    Vue路由懶加載是一種優(yōu)化技術(shù),旨在減少應(yīng)用程序的初始加載時間并提高性能,這篇文章給大家介紹vue路由懶加載的相關(guān)知識,感興趣的朋友跟隨小編一起看看吧
    2024-05-05
  • 解決webpack-bundle-analyzer的問題大坑

    解決webpack-bundle-analyzer的問題大坑

    這篇文章主要介紹了解決webpack-bundle-analyzer的問題大坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • vue中proxy代理的用法(解決跨域問題)

    vue中proxy代理的用法(解決跨域問題)

    這篇文章主要介紹了vue中的proxy代理的用法(解決跨域問題),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-12-12

最新評論