Home  >  Article  >  Web Front-end  >  Let's talk about the memory problem of keepalive in Vue

Let's talk about the memory problem of keepalive in Vue

青灯夜游
青灯夜游forward
2022-10-14 20:21:542936browse

Let's talk about the memory problem of keepalive in Vue

[Related recommendations: vuejs video tutorial]

1. Cause

Recently discovered the company Projects occasionally crash.

Lets talk about the memory problem of keepalive in Vue

#At first I thought it was some infinite loops written in the code, but I didn’t find it after checking.

Later, through performance inspection, it was found that the memory reached more than 1 G. It may be that the memory was not recycled normally, and the project happened after integrating from multi-page to single page. The single page uses keepalive internal tabs. accomplish. So the initial inference is that the memory may be overcrowded.

Locate the cause

View the current memory usage through performance -> memory,

  • By frantically opening and closing internal tabs, I found that the memory has reached an astonishing 2g, related operations have begun to become unresponsive, and the page is stuck or even white screen

Lets talk about the memory problem of keepalive in Vue

  • You can see through the command that 2g can be used, 2g has been used, and the cap is 4g

Lets talk about the memory problem of keepalive in Vue

  • # #The amazing thing is that after 2g, you can still continue to operate and play after a while, and the memory has reached 4g

Lets talk about the memory problem of keepalive in Vue

  • At this time, Barbie Q is already in progress. After pressing Enter in the console.log, there is no space to execute and output.

Lets talk about the memory problem of keepalive in Vue

2. Locating the problem

#1. Restoration scenario

Due to the complexity of the internal system code and the intersecting logic and hidden memory leak code . Comparing the company's other built-in multi-tab cache projects, similar problems also exist. Therefore, it is necessary to build a pure environment and analyze it from the bottom step by step. First restore the version environment used by the project.

2. Write a demo

First write a demo to reproduce the problem. Use vue-cli to create a project corresponding to version 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>新增(enter)</button> <button>删除(esc)</button> <button>强制垃圾回收(backspace)</button> <span>内存已使用<b></b></span>
        <div>
            <keep-alive>
                <router-view></router-view>
            </keep-alive>
        </div>
        <div>
            <div>
                <span>a{{index}}</span>
                <button>x</button>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    name: "App",
    data() {
        return {
            indexList: [],
            usedJSHeapSize: &#39;&#39;
        }
    },
    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 = [&#39;index&#39;]//
            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 !== &#39;/index&#39;) { 
                this.$router.push(&#39;/index&#39;) 
            }
            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>
.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:&#39;index&#39;,
}
</script>
view/a.vue

<template>
    <div>组件view<input> </div>
</template>
<script>
export default {
    name:&#39;A&#39;,
    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 effect

Lets talk about the memory problem of keepalive in Vue

  • Clicking Add will create an 80mb component. You can see that 4 new components have been added. Keepalive occupies about 330mb. (Real-time monitoring and performance interface calculations, memory diagnostic reports will be biased)

  • Clicking delete will remove the last element by default. You can also delete it through

    x on the element. Each time you delete it, you will jump back to the home page first. Make sure that the deleted view is not the view being displayed.

3. Reproduce the problem

1. After creating 4 components and deleting the last a4, At the same time, the memory is immediately recycled and the memory is not released. Still 328mb.

2. But when one more a3 is deleted, 80 is released, which makes people even more confused. Lets talk about the memory problem of keepalive in VueLets talk about the memory problem of keepalive in Vue

3. That’s not all. If I add 4 new ones and then delete the first ones, they can be released in real time.

Lets talk about the memory problem of keepalive in Vue

Good guy, vue’s official api is also So unreliable? For programmers, uncertain problems are much harder than actual errors.

I quickly checked the official website and found that vue2 fixed the keepalive problem in 2.6.13 between 2.6.12 and 2.7.10. Since 2.7.10 was rewritten using ts, and vue3 was introduced The compositionAPI, for stability, is only upgraded to the latest 2.6.14 of 2.6.

Lets talk about the memory problem of keepalive in Vue

Lets talk about the memory problem of keepalive in Vue

结果问题依然存在,于是又试了下2.7.10,结果还是一样的现象。

4.分析

4.1全局引用是否正常释放

在vue里,只有一个子节点App,再里面就是 keepalive 和 a1,a2,a3,a4 ,这5个是平级的关系

Lets talk about the memory problem of keepalive in Vue

Lets talk about the memory problem of keepalive in Vue

可以看到当删除a4的时候App里面的子节点只剩下keepalive 和 a1,a2,a3, 4个元素,所以这里没有内存问题。

1Lets talk about the memory problem of keepalive in Vue

4.2keepalive 的cache是否正常释放

可以看到cache集合里面已经移除a4的缓存信息

1Lets talk about the memory problem of keepalive in Vue

4.3挨个组件检查引用关系

  • 通过诊断报告搜索vuecomponent,可以看到有7个vuecomponent的组件(keepalive 和 App.vue  + index.vue +  自定义创建的4个动态a组件)

1Lets talk about the memory problem of keepalive in Vue

  • 通过鼠标移动到对应的vueVomponent上会显示对应的实例,如下图的a4实例

1Lets talk about the memory problem of keepalive in Vue

  • 现在我尝试删除a4,再生成报告2,在报告2中我们还是能看到a4,这时候内存就没有正常释放了

1Lets talk about the memory problem of keepalive in Vue

  • 并且发引用关系已经变成11层,与其他的5层不一样。点击改a4后,下面Object页签会展开显示正在引用他的对象

1Lets talk about the memory problem of keepalive in Vue

  • 鼠标移动到$vnode上看,发现居然是被a3组件引用了,这是为什么?

1Lets talk about the memory problem of keepalive in Vue

根据一层层关系最后发现

 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。

1Lets talk about the memory problem of keepalive in Vue

上面描述的各个组件的引用关系,a1-> a2 -> a3 -> a4 。 这也解释了为什么删除a1内存能够立即释放,同理继续删除a2 也是能正常释放。

但是如果先删除a4,由于a3引用着他所以不能释放a4。

3. 修复问题

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

Lets talk about the memory problem of keepalive in Vue

通过live server启动服务Lets talk about the memory problem of keepalive in Vue

这样每次修改源码,都会实时发布到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

nbsp;html>

  
    <meta>
    <meta>
    <meta>
    <link>favicon.ico">
    <title></title> 
     <!-- 这里是本地的vue源码 -->
    <script></script>
  
  
    <noscript>
    </noscript>
    <div></div>
    <!-- built files will be auto injected -->
  

这里cdn改成生成自己生成的vue sourcemap 实时地址。

4.调试代码

在开发者工具里,crtl+p 打开源码搜索框,输入keepalive,找到对应的源码。

Lets talk about the memory problem of keepalive in Vue

在render方法里打上断点,可以发现每当路由发送变化,keepalive的render方法都会重新渲染Lets talk about the memory problem of keepalive in Vue

打开源码

/* @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])
  }
}</function></string></string>

这里包含了整个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依然被保留着。NNDLets talk about the memory problem of keepalive in Vue点击后发现,是a4的dom已经没在显示,dom处于游离detach状态,看看是谁还引用着。好家伙,又是父节点keepalive的引用着,这次是elm。

Lets talk about the memory problem of keepalive in Vue于是在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
      }
    }
   .....
}

再次怀着喜悦的心情运行,发现这次靠谱了。

Lets talk about the memory problem of keepalive in Vue

nice~~

总结

  • 由于早期浏览器的架构都是一个页面html一个tab,所以很少会出现tab内存不够的情况。但是随着前端工程化的发展,单页面客户端渲染的应用也越来越普及。所以内存的问题也会日渐均增,对内存的优化与问题也会越来越多。

  • 当遇到偶发的奔溃问题时候,chrome的内存工具是个很好的帮手,可以快速生成报告并告知你引用的嵌套关系。

  • 分析问题还有一个好方法就是对比其他vue多页签项目是否存在内存泄露问题,结果发现一样存在。基于这个前提再去分析官方的代码。

  • 官方源码其实也提供了好像的调试环境,配合sourcemap对于分析定位和调试源码问题非常关键。

  • 当然改源码都是下策,最好的办法还是提issue。赶紧上githut 提个PR看看,从代码源头处理掉这个bug。

demo 源码地址github.com/mjsong07/vu…

issue addressgithub.com/vuejs/vue/i…

(Learning video sharing: web front-end development, programming basics video)

The above is the detailed content of Let's talk about the memory problem of keepalive in Vue. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete