您好,登錄后才能下訂單哦!
本篇內容介紹了“vue中keepalive的內存問題怎么解決”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
最近發現公司項目偶發性發生 奔潰現象。
剛開始以為是代碼寫了一些死循環,檢查完并未發現。
后面通過 performance 檢查 發現內存飚到了1個多G, 可能是內存沒有正常的回收,而項目是從多頁面整合到單頁面后發生的,單頁面使用的是keepalive 內部頁簽實現。所以初步推斷可能是內存擠爆了。
定位原因
通過performance -> memory 看到當前內存使用情況,
通過瘋狂的打開內部頁簽+關閉,發現內存已經達到驚人的2g,相關的操作已經開始無法響應,頁面卡頓甚至白屏
通過命令可以看到 可以使用2g,已使用2g, 封頂4g
神奇的是2g后,等一會依然可以繼續操作,繼續擼,內存已經懟到4g了
這時候已經芭比Q了,控制臺console.log回車后,都沒空間執行和輸出了
1.還原場景
由于內部系統代碼復雜并有交叉邏輯和隱性的內存泄露的代碼。對比了公司其他內置多頁簽緩存項目,也存在類似問題。所以需要搭建一個純凈的環境一步步從底層分析。 首先還原項目使用的版本環境。
2.寫個demo
先寫個demo重現問題。使用vue-cli創建項目對應版本 vue2.6.12, vue-router3.6.4
main.js
import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ render: h => h(App), router }).$mount('#app')
App.vue
<template> <div> <div>keep-alive includeList:{{indexNameList}}</div> <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">刪除(esc)</button> <button @click="gc()">強制垃圾回收(backspace)</button> <span >內存已使用<b id="usedJSHeapSize"></b></span> <div class="keepaliveBox"> <keep-alive :include="indexNameList"> <router-view /> </keep-alive> </div> <div class="barBox"> <div class="box" v-for="(index) in indexList" :key="index"> <span @click="routerClick(index)">a{{index}}</span> <button @click="routerDel(index)" title="刪除(esc)">x</button> </div> </div> </div> </template> <script> export default { name: "App", data() { return { indexList: [], usedJSHeapSize: '' } }, mounted() { const usedJSHeapSize = document.getElementById("usedJSHeapSize") window.setInterval(() => { usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb" }, 1000) // 新增快捷鍵模擬用戶實際 快速打開關閉場景 document.onkeydown = (event) => { event = event || window.event; if (event.keyCode == 13) {//新增 this.routerAdd() } else if (event.keyCode == 27) { //刪除 this.routerDel() } else if (event.keyCode == 8) { //垃圾回收 this.gc() } }; }, computed: { indexNameList() { const res = ['index']// this.indexList.forEach(index => { res.push(`a${index}`) }) return res } }, methods: { routerAdd() { let index = 0 this.indexList.length > 0 && (index = Math.max(...this.indexList)) index++ this.indexList.push(index) this.$router.$append(index) this.$router.$push(index) }, routerDel(index) { if (this.indexList.length == 0) return if(!index) { index = Math.max(...this.indexList) } //每次刪除都先跳回到首頁, 確保刪除的view 不是正在顯示的view if (this.$route.path !== '/index') { this.$router.push('/index') } let delIndex = this.indexList.findIndex((item) => item == index) this.$delete(this.indexList, delIndex) //延遲執行,加到下一個宏任務 // setTimeout(() => { // this.gc() // }, 100); }, routerClick(index) { this.$router.$push(index) }, gc(){ //強制垃圾回收 需要在瀏覽器啟動設置 --js-flags="--expose-gc",并且不打開控制臺,沒有效果 window.gc && window.gc() }, } }; </script> <style scoped> .keepaliveBox { border: 1px solid red; padding: 3px; } .barBox { display: flex; flex-wrap: wrap; } .box { margin: 2px; min-width: 70px; } .box>span { padding: 0 2px; background: black; color: #fff; } </style>
view/index.vue
<template> <div>首頁</div> </template> <script> export default { name:'index', } </script>
view/a.vue
<template> <div>組件view<input v-model="myname"/> </div> </template> <script> export default { name:'A', data(){ return { a:new Array(20000000).fill(1),//大概80mb myname:"" } }, mounted(){ this.myname = this.$route.query.name } } </script>
router/index.js
import Vue from 'vue' import Router from 'vue-router' import a from '../view/a.vue' Vue.use(Router) const router = new Router({ mode: 'hash', routes: [ { path: '/', redirect: '/index' }, { path: '/index', component: () => import('../view/index.vue') } ] }) //動態添加路由 router.$append = (index) => { router.addRoute(`a${index}`,{ path: `/a${index}`, component: { ...a, name: `a${index}` }, }) } router.$push = (index) => { router.push({ path:`/a${index}`, query:{ name:`a${index}` } }) } export default router
demo效果
點擊新增會創建一個80mb的組件,可以看到新增4個組件,keepalive占用大概330mb左右,(實時監控和performance接口計算,內存診斷報告會有偏差)
點擊刪除會默認移除最后一個元素,也可以通過元素上的x
來刪除,每次刪除都先跳回到首頁, 確保刪除的view 不是正在顯示的view。
3.重現問題
1.當創建4個組件后,刪除最后一個a4時候,同時立即回收內存,內存并沒有釋放。依然是328mb。
2.但是當再刪除多一個a3的時候 居然又釋放的80,讓人更加疑惑。
3.這還不算,如果我新增4個,然后先刪除最前面的居然能實時的釋放
好家伙,vue官方api也這么不靠譜嗎?對于程序員來說,不確定問題比實實在在的錯誤都要難得多。
趕緊上官網看了下,發現vue2 從2.6.12 到 2.7.10 之間 在 2.6.13 修復了 關于keepalive的問題,由于2.7.10使用ts重寫了,并且引入的vue3的compositionAPI,為了穩定,只升級到 2.6的最新2.6.14。
結果問題依然存在,于是又試了下2.7.10,結果還是一樣的現象。
4.分析
4.1全局引用是否正常釋放
在vue里,只有一個子節點App,再里面就是 keepalive 和 a1,a2,a3,a4 ,這5個是平級的關系
可以看到當刪除a4的時候App里面的子節點只剩下keepalive 和 a1,a2,a3, 4個元素,所以這里沒有內存問題。
4.2keepalive 的cache是否正常釋放
可以看到cache集合里面已經移除a4的緩存信息
4.3挨個組件檢查引用關系
通過診斷報告搜索vuecomponent,可以看到有7個vuecomponent的組件(keepalive 和 App.vue + index.vue + 自定義創建的4個動態a組件)
通過鼠標移動到對應的vueVomponent上會顯示對應的實例,如下圖的a4實例
現在我嘗試刪除a4,再生成報告2,在報告2中我們還是能看到a4,這時候內存就沒有正常釋放了
并且發引用關系已經變成11層,與其他的5層不一樣。點擊改a4后,下面Object頁簽會展開顯示正在引用他的對象
鼠標移動到$vnode上看,發現居然是被a3組件引用了,這是為什么?
根據一層層關系最后發現
a3組件.$vnode.parent.componentOptions.children[0] 引用著 a4
導致a4 無法正常釋放
基于這個點,查詢了前面a2,a3 也存在引用的關系,a1 正常無人引用它。
a2組件.$vnode.parent.componentOptions.children[0] 引用著 a3 a1組件.$vnode.parent.componentOptions.children[0] 引用著 a2 a1組件 正常,沒被引用
這里看到看出 a3組件.$vnode.parent 其實就是keepalive對象。
由于keepalive不參與渲染,但是每次組件渲染都會傳入componentOptions,componentOptions里面包含了當前的keepalive的信息,keepalive又包裹了上一次第一個渲染的子節點。
5.結論
當加載組件a1,a1對應的keepalive的componentOptions的children[0]信息也是a1。
當加載組件a2,a2對應的keepalive的componentOptions的children[0]信息也是a2,但是這時候上面的a1對應的keepalive由于是同一個引用,導致a1對應的keepalive的componentOptions信息也是a2。
當加載組件a3,a3對應的keepalive的componentOptions的children[0]信息也是a3,導致a2對應的keepalive的componentOptions信息也是a3。
當加載組件a4,a4對應的keepalive的componentOptions的children[0]信息也是a4,導致a3對應的keepalive的componentOptions信息也是a4。
上面描述的各個組件的引用關系,a1-> a2 -> a3 -> a4 。 這也解釋了為什么刪除a1內存能夠立即釋放,同理繼續刪除a2 也是能正常釋放。
但是如果先刪除a4,由于a3引用著他所以不能釋放a4。
1.思路
根據上面的關系我們指導,所有問題都是vue實例的時候關聯的keepalive引用了別的組件,我們只需要把keepalive上面componentOptions的children[0] 引用的關系切斷就ok了。這時候我們可以從vue的keepalive源碼入手調整。
2.構建可以定位具體源碼的環境
該項目使用的是vue 的cdn引入,所以只需要重新上傳一份支持sourcemap的并且沒有被混淆的vue庫即可。 通過--sourcemap 命令參數 生產支持源碼映射的代碼,以相對路徑的方式上傳的對應的cdn地址。參考地址
git clone --branch 2.6.14 https://github.com/vuejs/vue.git //拉取代碼
修改package.json,添加 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev",
本地運行
npm run dev
通過live server啟動服務
這樣每次修改源碼,都會實時發布到dist下的vue.js 我們就可以實時調試了訪問地址: 訪問地址:http://127.0.0.1:5500/dist/vue.js
3.改造現有項目成cdn
vue.config.js
module.exports = { chainWebpack: config => { config.externals({ vue: "Vue", }); }, configureWebpack: { devtool: "eval-source-map" }, lintOnSave: false };
public/index.html
<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> <!-- 這里是本地的vue源碼 --> <script src="http://127.0.0.1:5500/dist/vue.js"></script> </head> <body> <noscript> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
這里cdn改成生成自己生成的vue sourcemap 實時地址。
4.調試代碼
在開發者工具里,crtl+p 打開源碼搜索框,輸入keepalive,找到對應的源碼。
在render方法里打上斷點,可以發現每當路由發送變化,keepalive的render方法都會重新渲染
打開源碼
/* @flow */ import { isRegExp, remove } from 'shared/util' import { getFirstComponentChild } from 'core/vdom/helpers/index' type CacheEntry = { name: ?string; tag: ?string; componentInstance: Component; }; type CacheEntryMap = { [key: string]: ?CacheEntry }; function getComponentName (opts: ?VNodeComponentOptions): ?string { return opts && (opts.Ctor.options.name || opts.tag) } function matches (pattern: string | RegExp | Array<string>, name: string): boolean { if (Array.isArray(pattern)) { return pattern.indexOf(name) > -1 } else if (typeof pattern === 'string') { return pattern.split(',').indexOf(name) > -1 } else if (isRegExp(pattern)) { return pattern.test(name) } /* istanbul ignore next */ return false } function pruneCache (keepAliveInstance: any, filter: Function) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { const entry: ?CacheEntry = cache[key] if (entry) { const name: ?string = entry.name if (name && !filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } } } } function pruneCacheEntry ( cache: CacheEntryMap, key: string, keys: Array<string>, current?: VNode ) { const entry: ?CacheEntry = cache[key] if (entry && (!current || entry.tag !== current.tag)) { entry.componentInstance.$destroy() } cache[key] = null remove(keys, key) } const patternTypes: Array<Function> = [String, RegExp, Array] export default { name: 'keep-alive', abstract: true, props: { include: patternTypes, exclude: patternTypes, max: [String, Number] }, methods: { cacheVNode() { const { cache, keys, vnodeToCache, keyToCache } = this if (vnodeToCache) { const { tag, componentInstance, componentOptions } = vnodeToCache cache[keyToCache] = { name: getComponentName(componentOptions), tag, componentInstance, } keys.push(keyToCache) // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } this.vnodeToCache = null } } }, created () { this.cache = Object.create(null) this.keys = [] }, destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { this.cacheVNode() this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, updated () { this.cacheVNode() }, render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this const key: ?string = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest remove(keys, key) keys.push(key) } else { // delay setting the cache until update this.vnodeToCache = vnode this.keyToCache = key } vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } }
這里包含了整個keepalive的所有邏輯,
剛開始也以為是LRU的設置問題,測試后發現keepalive的數組都是能正常釋放。
懷疑是max最大長度限制,解決也是正常。 確保keepalive內部能正常釋放引用后,就要想如何修復這個bug,關鍵就是把children設置為空
組件.$vnode.parent.componentOptions.children = []
最合適的位置就在每次render的時候都重置一下所有錯誤的引用即可
代碼如下,把錯誤引用的children設置為空
render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) //修復緩存列表問題 for (const key in this.cache) { const entry: ?CacheEntry = this.cache[key] if (entry && vnode && entry.tag && entry.tag !== vnode.tag ) { //如果當前的緩存對象不為空 并且 緩存與當前加載不一樣 entry.componentInstance.$vnode.parent.componentOptions.children = [] } } ..... }
懷著喜悅的心情以為一切ok,運行后發現,a4依然被保留著。NND
點擊后發現,是a4的dom已經沒在顯示,dom處于游離detach狀態,看看是誰還引用著。好家伙,又是父節點keepalive的引用著,這次是elm。
于是在keepalive源碼的render方法加入
entry.componentInstance.$vnode.parent.elm = null
登錄后復制
整體代碼如下
render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) //修復緩存列表問題 for (const key in this.cache) { const entry: ?CacheEntry = this.cache[key] if (entry && vnode && entry.tag && entry.tag !== vnode.tag ) { //如果當前的緩存對象不為空 并且 緩存與當前加載不一樣 entry.componentInstance.$vnode.parent.componentOptions.children = [] entry.componentInstance.$vnode.parent.elm = null } } ..... }
再次懷著喜悅的心情運行,發現這次靠譜了。
“vue中keepalive的內存問題怎么解決”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。