本文包含以下章節(jié):
- 1、內(nèi)存泄露的排查點
- 2、案例一:樹組件的每個節(jié)點增加hover事件導致內(nèi)存泄露
- 3、案例二:表格組件的slot中使用表格行數(shù)據(jù)row作為內(nèi)聯(lián)函數(shù)的參數(shù)和動態(tài)判斷class的依據(jù)
- 4、案例三:表格組件的slot中使用表格行數(shù)據(jù)row作為參數(shù)傳遞給子組件
- 5、案例四:樹組件的子節(jié)點數(shù)據(jù)懶加載回來后,將數(shù)據(jù)綁定在當前點擊的父節(jié)點node上,此處直接修改treeData的一個節(jié)點數(shù)據(jù),導致舊的表格數(shù)據(jù)沒有釋放,出現(xiàn)內(nèi)存增長從而泄露
- 6、工具使用:chrome性能監(jiān)控
寫在最后的總結(jié):寫完整個經(jīng)驗總結(jié)后,發(fā)現(xiàn)4個案例有一個共同點:el-table、el-tree等組件的點擊事件或插槽slot中如果給你返回了行數(shù)據(jù)row、列頭信息column、節(jié)點數(shù)據(jù)node等參數(shù),千萬不要直接修改他們。如果有修改的業(yè)務場景,將要修改的數(shù)據(jù)先深拷貝一份然后修改拷貝的數(shù)據(jù),所有的修改,想要響應式的反映在頁面上時,一定是整個重置treeData或者tableData,而不能重置他們中的某一個行數(shù)據(jù)或者一個節(jié)點node數(shù)據(jù)。
// =====正確
// 克隆row
let rowCopy = cloneDeep(row)
// 修改克隆的row
rowCopy.editableField={label: '想修改的值'}
// 克隆tableData
let tableDataCopy = cloneDeep(tableData)
// 修改克隆的tableData
tableDataCopy[index] = rowCopy
// 重置tableData
tableData.value = [...tableDataCopy ]
// =====錯誤
row.editableField={label: '想修改的值'} // 錯誤:直接修改row
tableData[index] = rowCopy // 錯誤:直接修改tableData中的一行數(shù)據(jù)
一、內(nèi)存泄露的排查點
1、本次排查出的問題(每一個都對本次泄露問題有影響)
- 樹組件的slot中,不要給每個節(jié)點綁定div,可以考慮在父節(jié)點上綁定一個div然后每次調(diào)整顯示位置
- 樹組件的slot中,不要給每個節(jié)點綁定事件(如mouseenter、mouseleave、click),可以考慮用樣式替代或者在父節(jié)點上增加一次事件
- element-dialog泄露:dialog嵌套虛擬樹或者大型數(shù)據(jù)的組件時,一定要配置destroyOnClose為true,并且配合下面兩點使用才有效:在對話框關(guān)閉時數(shù)據(jù)對象一定要置為null、對話框內(nèi)容區(qū)域的組件或dom,一定要用v-if,跟隨對話框關(guān)閉時銷毀內(nèi)部組件
- 公共組件虛擬樹中,引用超大的國際化文件,導致內(nèi)存泄露(虛擬樹被嵌套在對話框中才會暴露該問題)
- table、tree組件使用時,v-if的判斷中不能有data數(shù)據(jù)或data.length>0這樣的判斷
- template中不要使用箭頭函數(shù),比如:<div @click='e => func(e, row.id)'>
- template中函數(shù)的參數(shù),不要使用數(shù)據(jù)對象,比如:<div @click='e => func(scope.row)'>,因為vue渲染時一直使用該變量,導致dom元素無法釋放
- 樣式類盡量提前定義好,不要使用數(shù)據(jù)作為參數(shù)來動態(tài)計算樣式類,如<div :class='{"aClass": scope.row.isA}'>。因為vue渲染時一直使用該變量,導致dom元素無法釋放
- 閉包導致的泄露(后面補充案例:websocket封裝過程中,回調(diào)函數(shù)未釋放導致泄露)
補充概念:內(nèi)聯(lián)函數(shù)(inline function)在 Vue 模板中指的是直接在模板中定義的函數(shù)表達式,它們通常會導致性能問題和潛在的內(nèi)存泄漏。常見形式包括以下3種:
<!-- 1. 事件處理中的內(nèi)聯(lián)函數(shù) -->
<button @click="() => doSomething(item)">按鈕</button>
<!-- 2. 插值中的內(nèi)聯(lián)函數(shù) -->
<div>{{ item.value.toFixed(2) }}</div>
<!-- 3. 計算屬性中的內(nèi)聯(lián)邏輯 -->
<div :class="{ active: isActive(item.id) }"></div>
2、常見內(nèi)存泄露排查點
- 全局變量在onUnmounted中置為null,完成清理,尤其是大型數(shù)據(jù)
- 定時器在onUnmounted中置為null完成清理,如setTimeout和setInterval
- dom和事件要在onBeforeUnmount中銷毀,變量在onUnmounted中銷毀
- 動態(tài)添加的dom元素,要及時清理
- 深度拷貝,用lodash-es的cloneDeep方法代替Json.parse(Json.stringfy(aa))
- 用WeakMap 替代對象緩存字符串
// 不推薦
const cache = {};
function process(data) {
cache[data.id] = JSON.stringify(data); // 無限增長
}
// 推薦
const cache = new WeakMap();
function process(obj) {
const str = JSON.stringify(obj);
cache.set(obj, str); // 隨對象自動回收
}
- 數(shù)組 join 替代字符串拼接
- 固定對象結(jié)構(gòu)
// 不推薦:創(chuàng)建大量不同形狀的對象
function createUser() {
const obj = {};
obj[Math.random().toString(36)] = true;
return obj;
}
// 推薦始終使用相同屬性結(jié)構(gòu)
function createUser() {
return {
id: null,
name: null,
// 明確所有可能字段
};
}
- 使用 Map 替代動態(tài)對象
// 不推薦:同一變量賦值不同形狀對象
let obj;
if (Math.random() > 0.5) {
obj = { a: 1 };
} else {
obj = { b: 1, c: 2 }; // 不同shape
}
// 推薦
const obj = new Map();
obj.set('dynamicKey', true); // 不影響對象shape
- 清除或重置store中的變量
二、問題現(xiàn)象以及定位過程:案例一
1、問題現(xiàn)象:頁面是左樹右表,樹組件使用的是我們自己基于el-tree封裝的組件,額外增加的功能是,鼠標hover事件會觸發(fā)節(jié)點的增刪改按鈕浮動框出現(xiàn),用來增刪改當前樹節(jié)點;同時,點擊樹節(jié)點后,右側(cè)表格內(nèi)容會更新。問題現(xiàn)象是,每次點擊樹節(jié)點,會發(fā)現(xiàn)內(nèi)存增長過快


2、技術(shù)使用:vue3+element-plus
3、問題定位思路和過程:
1、排查點擊事件函數(shù)是否有dom的處理,沒有發(fā)現(xiàn)問題
2、排查template中的對事件的寫法,發(fā)現(xiàn)每個節(jié)點都綁定了mouseenter和mouseleave事件,用來顯示增刪改按鈕組。驗證后發(fā)現(xiàn),確實是這兩個鼠標事件導致內(nèi)存快速增長。判斷依據(jù)是,鼠標只需要在樹節(jié)點上滑動,并不需要點擊,就能看到內(nèi)存快速增長到500M
3、每一層節(jié)點上都綁定了該事件,并且使用v-show默認隱藏按鈕組div。實際業(yè)務上,只需要對最外出節(jié)點顯示按鈕組,其他子節(jié)點不需要

4、分析:一、不應該給每個節(jié)點增加事件,應該替換成在父節(jié)點上增加一個事件,然后使用冒泡去處理業(yè)務邏輯,減少事件觸發(fā);或者采用hover樣式替代事件。二、不需要的節(jié)點上,不增加額外的事件,只給最外層增加即可;三、用v-if替換v-show,減少不必要的dom渲染。優(yōu)化后的代碼如下所示:


三、問題現(xiàn)象以及定位過程:案例二
1、問題現(xiàn)象:有一個表格,單元格中有一個按鈕,點擊按鈕會彈出對話框。對話框左邊是虛擬樹,右邊是表格


2、技術(shù)使用:vue3+element-plus
3、問題定位思路和過程:
1、排查我們自己封裝的虛擬樹,沒有泄露
2、排查我們自己封裝的對話框,有泄漏:對話框內(nèi)嵌套5萬個div,關(guān)閉時不釋放dom,得出結(jié)論:我們封裝的對話框有泄漏
2、排查element的對話框,有泄漏:對話框內(nèi)嵌套5萬個div,關(guān)閉時不釋放dom,要配置destroyOnClose,并且5萬個div外嵌套一個總的div,總的div上用v-if=dialogVisble在關(guān)閉對話框時銷毀全部div。得出結(jié)論:element的對話框本身有泄漏,需要配置屬性,并且嵌套內(nèi)容要配合v-if;我們自己封裝的對話框,也要這樣修改
3、我們封裝的對話框中嵌套elment原生的虛擬樹,有泄漏,需要把虛擬樹的treeData對象在關(guān)閉對話框時置為空數(shù)組或者null。得出結(jié)論:element的對話框依然有泄漏,需要把treeData在關(guān)閉時置為空數(shù)組或者null
4、修改以上問題后,用我們封裝的對話框中嵌套我們封裝的虛擬樹,依然有泄露,二分法刪除我們封裝的虛擬樹組件的代碼,最后定位原因是,引用了一個超大的國際化文件得出結(jié)論:對話框嵌套我們封裝的虛擬樹有問題,要刪除國際化文件的引用
5、-------------至此,我們封裝的對話框嵌套我們封裝的虛擬樹,已經(jīng)沒有問題??梢曰氐絾栴}暴露點上了
6、點擊表格單元格內(nèi)的按鈕,打開對話框,有泄漏,刪除對話框,多個表格切換,仍然有內(nèi)存泄露,說明表格本身有泄漏。得出結(jié)論:表格使用中有泄漏問題
7、二分法刪除表格的業(yè)務代碼,最后發(fā)現(xiàn)表格的slot寫法中有多處泄露(代碼可參考后面的截圖)。一個是,div的樣式類是動態(tài)的,它使用了表格數(shù)據(jù)來判斷是否有該樣式類;另一個是,表格的一列單元格的內(nèi)容是字典,需要根據(jù)字典的code查找出對應的label。得出結(jié)論:打開對話框出現(xiàn)內(nèi)存泄露只是表象,在這個問題中,任何添加dom的操作,都會導致dom只增不減,因此切換多個表格也會出現(xiàn)同樣的泄露問題,表格出了問題,不是對話框出了問題;更重要的一點是,table組件的slot中,盡量不要使用渲染數(shù)據(jù)來做判斷、也不要作為內(nèi)聯(lián)函數(shù)的參數(shù)


8、錯誤修復
問題一如圖,數(shù)據(jù)中提前計算好樣式類,table組件的slot中直接使用,不再計算


問題二如圖:表格渲染時,內(nèi)聯(lián)函數(shù)中不要把渲染的行數(shù)據(jù)作為參數(shù)傳遞,只傳遞行數(shù)據(jù)的下表index,然后行數(shù)據(jù)可以通過tableData[index]方式獲取到


注意:內(nèi)聯(lián)函數(shù)中,當index為-1時,或者tableData沒有數(shù)據(jù)時,一定要返回,否則依然會泄露,這個點比較隱秘,很難發(fā)現(xiàn)
四、案例三:給子組件傳參引發(fā)的內(nèi)存泄露
如下圖,左側(cè)是有問題的代碼,右側(cè)是修改后的。問題仍然是將表格的某一行數(shù)據(jù)作為參數(shù)傳遞給子組件。無論子組件是否修改了改參數(shù),都會導致dom不釋放。右側(cè)是修改后的代碼,修改思路依然是,不傳遞表格的行數(shù)據(jù),可以傳遞表格全部數(shù)據(jù)以及當前行的下標,子組件根據(jù)這兩個參數(shù)就能獲取到當前行的數(shù)據(jù)。

五、案例四:樹組件的子節(jié)點數(shù)據(jù)懶加載回來后,將數(shù)據(jù)綁定在當前點擊的父節(jié)點node上,此處直接修改treeData的一個節(jié)點數(shù)據(jù),導致舊的表格數(shù)據(jù)沒有釋放,出現(xiàn)內(nèi)存增長從而泄露。

六、工具使用:chrome性能監(jiān)控
1、兩種方式,第一個比較難用,第二個比較常用
1、chrom的開發(fā)者模式中,memory頁簽下,可以抓取兩次快照,對比內(nèi)存泄露的大小、泄露的對象、泄露的文件和代碼。
【點評】親測不好用,因為vue3組件嵌套過深,依賴的框架和第三方較多,對比出來的維度和條目有十幾萬條,并且結(jié)果中將插件和自己編譯后的代碼混合在一起,很難找到自己寫的文件的泄露點,總體來說,就像大海撈針
2、chrom的devTools中,右上角的三個點標志中,找到more tools中的performance monitor,然后結(jié)合memory頁簽下的回收內(nèi)存按鈕,就可以監(jiān)控js內(nèi)存、dom大小和事件監(jiān)聽。js內(nèi)存和dom持續(xù)上漲,一定有泄露,最實用也是最靠譜的辦法是,二分法刪除代碼,不斷縮小范圍,最后鎖定問題。
【點評】性能監(jiān)控工具,就是一個指南針,給出一個方向和泄露體量的指標值。要解決具體問題,還是得逐行查看自己的代碼。這個過程一定會非常耗時,做好準備,踏實使用二分法對代碼地毯式排查,沒有捷徑。因為每一個問題都有可能與之前的不一樣,之前積累的經(jīng)驗在關(guān)鍵的時候有用。但是,達到關(guān)鍵點之前,大量的排查工作,將問題鎖定到很小的范圍之內(nèi),是必不可少的,少了排查,關(guān)鍵點不可能到來
2、方式一使用說明
兩次快照對比時,字段的意義如下:
- alloc.size (Allocated Size)
定義:在兩次快照之間新分配的內(nèi)存總量
計算方式:sum(所有新創(chuàng)建對象的內(nèi)存大小)
正常情況:在操作過程中會有合理的新內(nèi)存分配
異常信號:持續(xù)高 alloc.size 且不被釋放可能表示泄漏
- freed size (Freed Size)
定義:在兩次快照之間被垃圾回收的內(nèi)存總量
計算方式:sum(所有被銷毀對象的內(nèi)存大小)
健康模式:應該與 alloc.size 保持相對平衡
危險信號:長期低 freed size 表明對象未被正確釋放
- size delta (Size Delta)
定義:內(nèi)存凈變化量
計算公式:size delta = alloc.size - freed.size
解讀:
0:內(nèi)存凈增長(潛在泄漏)
≈0:內(nèi)存平衡(健康狀態(tài))
<0:內(nèi)存釋放(可能是緩存清理)
內(nèi)存泄漏判斷標準-確定泄漏的強信號:
連續(xù)多次快照對比都顯示 size delta > 0
freed size ≈ 0 而 alloc.size 持續(xù)增長
特定對象類型的 delta 持續(xù)為正