您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關(guān)Vue頁面中出現(xiàn)內(nèi)存泄露的原因有哪些,小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
什么是內(nèi)存泄露??jī)?nèi)存泄露是指new了一塊內(nèi)存,但無法被釋放或者被垃圾回收。new了一個(gè)對(duì)象之后,它申請(qǐng)占用了一塊堆內(nèi)存,當(dāng)把這個(gè)對(duì)象指針置為null時(shí)或者離開作用域?qū)е卤讳N毀,那么這塊內(nèi)存沒有人引用它了在JS里面就會(huì)被自動(dòng)垃圾回收。但是如果這個(gè)對(duì)象指針沒有被置為null,且代碼里面沒辦法再獲取到這個(gè)對(duì)象指針了,就會(huì)導(dǎo)致無法釋放掉它指向的內(nèi)存,也就是說發(fā)生了內(nèi)存泄露。為什么代碼里面會(huì)拿不到這個(gè)對(duì)象指針了呢,舉一個(gè)例子:
// 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這個(gè)變量就一會(huì)直存在了,直到你把頁面關(guān)了,因?yàn)閐ate的引用是在另一個(gè)module里面,可以理解為模塊就是一個(gè)閉包對(duì)外是不可見的。所以如果你是希望這個(gè)date對(duì)象一直存在、需要一直使用的話,那么沒有問題,但是如果想用一次就不用了那就會(huì)有問題,這個(gè)對(duì)象一直在內(nèi)存里面沒有被釋放就發(fā)生了內(nèi)存泄露。
另一種比較隱蔽并且很常見的內(nèi)存泄露是事件綁定,形成了一個(gè)閉包,導(dǎo)致一些變量一直存在。如下例子所示:
// 一個(gè)圖片懶惰加載引擎示例 class ImageLazyLoader { constructor ($photoList) { $(window).on('scroll', () => { this.showImage($photoList); }); } showImage ($photoList) { $photoList.each(img => { // 通過位置判斷圖片滑出來了就加載 img.src = $(img).attr('data-src'); }); } } // 點(diǎn)擊分頁的時(shí)候就初始化一個(gè)圖片懶惰加載的 $('.page').on('click', function () { new ImageLazyLoader($('img.photo')); });
這是一個(gè)圖片懶惰加載的模型,每次點(diǎn)分頁的時(shí)候就會(huì)清掉上一頁的數(shù)據(jù)更新為當(dāng)前頁的DOM,并重新初始化一個(gè)懶惰加載的引擎。它里面監(jiān)聽了scroll事件,對(duì)傳進(jìn)來的圖片列表的DOM進(jìn)行處理。每點(diǎn)一次分頁就會(huì)重新new一個(gè),這里就發(fā)生了內(nèi)存泄露,主要是以下3行代碼導(dǎo)致的:
$(window).on('scroll', () => { this.showImage($photoList); });
因?yàn)檫@里的事件綁定形成了一個(gè)閉包,this/$photoList這兩個(gè)變量一直沒有被釋放,this是指向ImageLazyLoader的實(shí)例,而$photoList是指向DOM結(jié)點(diǎn),當(dāng)清除掉上一頁的數(shù)據(jù)的時(shí)候,相關(guān)DOM結(jié)點(diǎn)已經(jīng)從DOM樹分離出來了,但是仍然還有一個(gè)$photoList指向它們,導(dǎo)致這些DOM結(jié)點(diǎn)無法被垃圾回收一直在內(nèi)存里面,就發(fā)生了內(nèi)存泄露。由于this變量也被閉包困住了沒有被釋放,所以還有一個(gè)ImageLazyLoader的實(shí)例發(fā)生內(nèi)存泄露。
這個(gè)的解決方法比較簡(jiǎn)單,就是銷毀實(shí)例的時(shí)候把綁定的事件off掉,如下代碼所示:
class ImageLazyLoader { constructor ($photoList) { this.scrollShow = () => { this.showImage($photoList); }; $(window).on('scroll', this.scrollShow); } // 新增一個(gè)事件解綁 clear () { $(window).off('scroll', this.scrollShow); } showImage ($photoList) { $photoList.each(img => { // 通過位置判斷圖片滑出來了就加載 img.src = $(img).attr('data-src'); }); // 判斷如果圖片已全部顯示,就把事件解綁了 if (this.allShown) { this.clear(); } } } // 點(diǎn)擊分頁的時(shí)候就初始化一個(gè)圖片懶惰加載的 let lazyLoader = null; $('.page').on('click', function () { lazyLoader && (lazyLoader.clear()); lazyLoader = new ImageLazyLoader($('img.photo')); });
在每次實(shí)例化一個(gè)ImageLazyLoader之前把先把上一個(gè)實(shí)例clear掉,clear里面進(jìn)行解綁,由于JS有構(gòu)造函數(shù)但是沒有解構(gòu)函數(shù),所以需要自己寫一個(gè)clear,在外面手動(dòng)調(diào)一下clear。同時(shí)在事件的執(zhí)行過程的合適時(shí)機(jī)自動(dòng)把事件給解綁了,上面是判斷如果所有的圖片都展示出來了那么就沒必要監(jiān)聽scroll事件了直接解綁了。這樣就能解決內(nèi)存泄露的問題了,能夠觸發(fā)自動(dòng)垃圾回收。
為什么把事件解綁了,就不會(huì)有閉包引用了呢?因?yàn)镴S引擎檢測(cè)到那個(gè)閉包沒用了,就把那個(gè)閉包銷毀了,那么閉包引用的外部變量也自然會(huì)被置空。
好了,基礎(chǔ)知識(shí)就講解到這里,現(xiàn)在用Chrome devtools的內(nèi)存檢測(cè)工具來實(shí)際操作一遍,方便發(fā)現(xiàn)頁面的一些內(nèi)存泄露行為。為了避免裝給瀏覽器裝的一些插件造成影響,使用Chome的隱身模式頁面,它會(huì)把所有的插件都給禁掉。
然后打開devtools,切到Memory的tab,選中Heap snapshot,如下所示:
什么叫heap snapshot呢?翻譯一下就是堆快照,給當(dāng)前內(nèi)存堆拍一張照片。因?yàn)閯?dòng)態(tài)申請(qǐng)的內(nèi)存都是在堆里面的,而局部變量是在內(nèi)存棧里面,是由操作系統(tǒng)分配管理的是不會(huì)內(nèi)存泄露了。所以關(guān)心堆的情況就好了。
然后做一些增刪改DOM的操作,如:
(1)彈一個(gè)框,然后把彈框給關(guān)了
(2)單頁面的點(diǎn)擊跳轉(zhuǎn)到另一個(gè)路由,然后再點(diǎn)后退返回
(3)點(diǎn)擊分頁觸發(fā)動(dòng)態(tài)改DOM
就是先增加DOM,然后把這些DOM給刪了,看一下這些被刪除的DOM是否還有對(duì)象引用它們。
這里我是第2種方式的場(chǎng)景,檢測(cè)單頁面應(yīng)用的某個(gè)路由頁面是否存在內(nèi)存泄露。先打開首頁,點(diǎn)到另一個(gè)頁面,再點(diǎn)后退,接著點(diǎn)一下垃圾回收的按鈕:
觸發(fā)垃圾回收,避免一些不必要的干擾。
然后再點(diǎn)一下拍照按鈕:
它就會(huì)把當(dāng)前頁面的內(nèi)存堆掃描一遍顯示出來,如下圖所示:
然后在上面中間的Class Filter的搜索框里搜一下detached:
它就會(huì)顯示所有已經(jīng)分離了DOM樹的DOM結(jié)點(diǎn),重點(diǎn)關(guān)注distance值不為空的,這個(gè)distance表示距離DOM根結(jié)點(diǎn)的距離。上圖展示的這些div具體是啥呢?我們把鼠標(biāo)放上去不動(dòng)等個(gè)2s,它就會(huì)顯示這個(gè)div的DOM信息:
通過className等信息可以知道它就是那個(gè)要檢查的頁面的DOM節(jié)點(diǎn),在下面的Object的窗口里面依次展開它的父結(jié)點(diǎn),可以看到它最外面的父結(jié)點(diǎn)是一個(gè)VueComponent實(shí)例:
下面黃色字體native_bind表示有個(gè)事件指向了它,黃色表示引用仍然生效,把鼠標(biāo)放到native_bind上面停留2秒:
它會(huì)提示你是在homework-web.vue這個(gè)文件有一個(gè)getScale函數(shù)綁定在了window上面,查看一下這個(gè)文件確實(shí)是有一個(gè)綁定:
mounted () { window.addEventListener('resize', this.getScale); }
所以雖然Vue組件把DOM刪除了,但是還有個(gè)引用存在,導(dǎo)致組件實(shí)例沒有被釋放,組件里面又有一個(gè)$el指向DOM,所以DOM也沒有被釋放。
但是看代碼的話是在beforeDestroyed里面解綁的:
beforeDestroyed () { window.removeEventListener('resize', this.getScale); }
所以應(yīng)該沒有問題???
定睛一看,傻眼了,原來函數(shù)名寫錯(cuò)了,應(yīng)該是:
beforeDestroy () { window.removeEventListener('resize', this.getScale); },
發(fā)現(xiàn)了一個(gè)隱藏多日的bug,因?yàn)檫@個(gè)比較隱蔽,就算寫錯(cuò)了也不會(huì)有明顯的感知了。
把這個(gè)地方改一下,重復(fù)操作一遍,再拍一張內(nèi)存快照。我們發(fā)現(xiàn)游離的div節(jié)點(diǎn)仍然是74個(gè)且disance不為空,沒有改進(jìn)如下圖所示:
難道剛剛改得不對(duì)?繼續(xù)查看剛剛第2個(gè)節(jié)點(diǎn):
可以發(fā)現(xiàn),這次是有一個(gè) 事件總線EventBus的事件綁定指向了它 ,說明除了剛剛那個(gè)resize事件綁定之外,還有一個(gè)EventBus的事件沒有釋放,事件名稱是gToNextHomworkTask。我們搜一下這個(gè)事件是在哪里綁的,可以找到它是在路由組件的一個(gè)子組件里面綁的:
mounted () { EventBus.$on('goToNextHomeworkTask', this.go2NextQuestion); }
果不其然,這個(gè)組件只有$on,沒有$off,所以導(dǎo)致組件卸載的時(shí)候仍然有一個(gè)事件的引用。所以需要在這個(gè)組件的destroyed里面給$off掉:
mounted () { EventBus.$off('goToNextHomeworkTask', this.go2NextQuestion); }
改完后刷新頁面操作第3次,再拍一張內(nèi)存快照,比較尷尬的是情況還是一樣:
說明還有人引用它,繼續(xù)查看是誰引用了沒有釋放:
可以發(fā)現(xiàn)是一個(gè) Vuex的$store的watch監(jiān)聽沒有釋放 ,借助Watcher的cb屬性可以知道具體是哪個(gè)監(jiān)聽函數(shù)。利用簡(jiǎn)單的文本搜索發(fā)現(xiàn)是在一個(gè)子組件里面進(jì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里面有一個(gè)this指針指向了組件的DOM元素,由于子組件沒有被釋放,那么包含它的父組件自然不會(huì)被釋放,所以一層層往上,導(dǎo)致最外面那個(gè)路由組件也不會(huì)被釋放。
這個(gè)需要在destroyed的時(shí)候unwatch一下:
mounted () { this.unwatchStore = this.$store.watch(state => state.currentIndex, (newIndex, oldIndex) => { // 代碼略 }); }, destroyed () { this.unwatchStore(); }
處理完之后再拍一張內(nèi)存快照,如下圖所示:
雖然還是74個(gè)但是distance已經(jīng)為空了,可對(duì)比前3步distance都不為空,并且下面Object展開沒有找到標(biāo)黃的部分了,也就是說這個(gè)路由組件內(nèi)存泄露的問題已經(jīng)得到解決。
我們繼續(xù)查看其它distance不為空的div節(jié)點(diǎn),如下圖所示,可以按照distance排下序:
其中有一個(gè)是.animate-container:
它是一個(gè)用來放lottie動(dòng)畫的DOM容器,lottie對(duì)象里面仍有引用它:
這個(gè)是一個(gè)用lottie做的loading動(dòng)畫,當(dāng)loading結(jié)束的時(shí)候,我會(huì)手動(dòng)調(diào)一下它的stop api停止動(dòng)畫,并且把.animte-container給remove掉,但是為什么lottie還不肯放過它呢?我的代碼是這么寫的:
let loadingAnimate = null; let bodymovinAnimate = { // 顯示loading動(dòng)畫 showLoading () { loadingAnimate = bodymovinAnimate._showAnimate(); return loadingAnimate; }, // 停止loading動(dòng)畫 stopLoading () { loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate); }, // 開始lottie動(dòng)畫 _showAnimate () { const animate = lottie.loadAnimation({ // 參數(shù)省略 }); return animate; } // 結(jié)束lottie動(dòng)畫 _stopAnimate (animate) { animate.stop(); let $container = $(animate.wrapper).closest('.bodymovin-container'); $container.remove(); }, }; export default bodymovinAnimate;
我猜想是調(diào)了stop之后lottie仍然沒有釋放對(duì)DOM的引用,因?yàn)閟top之后還能夠夠支持重新start的,所以它得咬著DOM不放,因此如果要徹底結(jié)束動(dòng)畫,應(yīng)該不是調(diào)stop,查了一下它還有一個(gè)destroy的方法,把stop換成destroy:
// 結(jié)束lottie動(dòng)畫 _stopAnimate (animate) { animate.destroy(); let $container = $(animate.wrapper).closest('.bodymovin-container'); $container.remove(); },
這樣改了之后,lottie的引用就會(huì)把它給釋放了,問題解決了,然后再重新拍一張照片:
仍然有一個(gè)exports.default指向它,它是webpack的模塊,我猜想是因?yàn)楸疚拈_篇提到的例子的原因,就是模塊形成了閉包,它的變量沒有被釋放造成內(nèi)存泄露,所以在stopLoading里面把它置成null:
// 停止loading動(dòng)畫 stopLoading () { loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate); loadingAnimate = null; },
這樣試了之后,.animate-container這個(gè)DOM對(duì)象就沒有人引用它了。
最后div還剩下3個(gè)有distance:
其中兩個(gè)是jq的$.support.boxSizingReliable,是jq用來檢測(cè)boxszing是否可用創(chuàng)建的div:
還有一個(gè)是Vue的:
這些都是使用的庫造成的內(nèi)存泄露,暫時(shí)先不管。
再去分析其它的標(biāo)簽也有類似的情況。
所以綜合上面的分析,造成內(nèi)存泄露的可能會(huì)有以下幾種情況:
(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)存分析工具進(jìn)行快速排查,本文主要是用到了內(nèi)存堆快照的基本功能,讀者可以嘗試分析自己的頁面是否存在內(nèi)存泄漏,方法是做一些操作如彈個(gè)框然后關(guān)了,拍一張堆快照,搜索detached,按distance排序,把非空的節(jié)點(diǎn)展開父級(jí),找到標(biāo)黃的字樣說明,那些就是存在沒有釋放的引用。也就是說這個(gè)方法主要是分析仍然存在引用的游離DOM節(jié)點(diǎn)。因?yàn)轫撁娴膬?nèi)存泄露通常是和DOM相關(guān)的,普通的JS變量由于有垃圾回收所以一般不會(huì)有問題,除非使用閉包把變量困住了用完了又沒有置空。
DOM相關(guān)的內(nèi)存泄露通常也是因?yàn)殚]包和事件綁定引起的。綁了(全局)事件之后,在不需要的時(shí)候需要把它解綁。當(dāng)然直接綁在div上面的可以直接把div刪了,綁在它上面的事件就自然解綁了。
以上就是Vue頁面中出現(xiàn)內(nèi)存泄露的原因有哪些,小編相信有部分知識(shí)點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見到或用到的。希望你能通過這篇文章學(xué)到更多知識(shí)。更多詳情敬請(qǐng)關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。