Maison  >  Article  >  interface Web  >  Parlons du problème de mémoire de keepalive dans Vue

Parlons du problème de mémoire de keepalive dans Vue

青灯夜游
青灯夜游avant
2022-10-14 20:21:542936parcourir

Parlons du problème de mémoire de keepalive dans Vue

【Recommandation associée : tutoriel vidéo vuejs

1. Cause

Récemment, il a été découvert que les projets de l'entreprise tombaient parfois en panne.

Parlons du problème de mémoire de keepalive dans Vue

Au début, je pensais qu'il s'agissait de boucles infinies écrites dans le code, mais je ne les ai pas trouvées après vérification.

Plus tard, grâce à l'inspection des performances, il a été constaté que la mémoire atteignait plus de 1 G. Il se peut que la mémoire n'ait pas été recyclée normalement et que le projet ait eu lieu après l'intégration du projet de plusieurs pages à une seule page. la page utilise l'implémentation de l'onglet interne keepalive. La première conclusion est donc que la mémoire est peut-être surchargée. "Localisez la cause" Impossible de répondre, la page est bloquée ou même l'écran est blanc

Vous pouvez voir à travers la commande que 2g peuvent être utilisés, 2g ont été utilisés et le capuchon est 4g

Parlons du problème de mémoire de keepalive dans Vue

La magie c'est qu'après 2g, attendez un instant Vous pouvez toujours continuer à fonctionner et à jouer, la mémoire a atteint 4g

Parlons du problème de mémoire de keepalive dans Vue

A ce moment, Barbie Q est déjà en place. Entrez dans le console.log, il n'y a pas d'espace pour l'exécution et la sortie

Parlons du problème de mémoire de keepalive dans Vue2 Localisation du problème

  • 1. Restaurez la scène

Parlons du problème de mémoire de keepalive dans VueEn raison de la complexité du système interne. le code et le code avec une logique croisée et des fuites de mémoire cachées. En comparant les autres projets de cache multi-onglets intégrés de la société, des problèmes similaires existent également. Il est donc nécessaire de construire un environnement pur et de l’analyser de fond en comble, étape par étape. Restaurez d’abord l’environnement de version utilisé par le projet.

2. Écrivez une démo

Écrivez d'abord une démo pour reproduire le problème. Utilisez vue-cli pour créer un projet correspondant à la version vue2.6.12, vue-router3.6.4main.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

Cliquer sur Ajouter créera un composant de 80 Mo. Vous pouvez voir que 4 nouveaux composants ont été ajoutés. Le keepalive occupe environ 330 Mo (surveillance en temps réel et calcul de l'interface de performance). , rapport de diagnostic de mémoire Il y aura des écarts)

Cliquer sur Supprimer supprimera le dernier élément par défaut. Vous pouvez également le supprimer via

sur l'élément. Chaque fois que vous le supprimez, vous reviendrez d'abord à la page d'accueil. assurez-vous que la vue supprimée n’est pas la vue actuellement affichée.

Parlons du problème de mémoire de keepalive dans Vue

  • 3. Reproduisez le problème

  • 1. Après avoir créé 4 composants et supprimé le dernier a4, la mémoire est immédiatement recyclée et la mémoire n'est pas libérée. Toujours 328 Mo.

    x

    2. Mais lorsqu'un a3 supplémentaire est supprimé, 80 sont libérés, ce qui rend les gens encore plus confus.

3. Ce n'est pas tout. Si j'en ajoute 4 nouveaux et que je supprime ensuite les premiers, ils peuvent être publiés en temps réelHé les gars, l'API officielle de Vue est-elle si peu fiable ? Pour les programmeurs, les problèmes incertains sont bien plus difficiles que les erreurs réelles.

Dépêchez-vous de consulter le site officiel et constatez que vue2 a résolu le problème de keepalive dans la version 2.6.13 entre la version 2.6.12 et la version 2.7.10. Depuis que la version 2.7.10 a été réécrite à l'aide de ts et que l'API de composition de vue3 a été introduite, dans l'ordre Stable. , mis à niveau uniquement vers la dernière version 2.6.14 de 2.6.

Parlons du problème de mémoire de keepalive dans VueParlons du problème de mémoire de keepalive dans Vue

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

4.分析

4.1全局引用是否正常释放

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

Parlons du problème de mémoire de keepalive dans Vue

Parlons du problème de mémoire de keepalive dans Vue

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

1Parlons du problème de mémoire de keepalive dans Vue

4.2keepalive 的cache是否正常释放

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

1Parlons du problème de mémoire de keepalive dans Vue

4.3挨个组件检查引用关系

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

1Parlons du problème de mémoire de keepalive dans Vue

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

1Parlons du problème de mémoire de keepalive dans Vue

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

1Parlons du problème de mémoire de keepalive dans Vue

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

1Parlons du problème de mémoire de keepalive dans Vue

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

Parlons du problème de mémoire de keepalive dans 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。

Parlons du problème de mémoire de keepalive dans 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

Parlons du problème de mémoire de keepalive dans Vue

通过live server启动服务Parlons du problème de mémoire de keepalive dans 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,找到对应的源码。

Parlons du problème de mémoire de keepalive dans Vue

在render方法里打上断点,可以发现每当路由发送变化,keepalive的render方法都会重新渲染Parlons du problème de mémoire de keepalive dans 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依然被保留着。NNDParlons du problème de mémoire de keepalive dans Vue点击后发现,是a4的dom已经没在显示,dom处于游离detach状态,看看是谁还引用着。好家伙,又是父节点keepalive的引用着,这次是elm。

Parlons du problème de mémoire de keepalive dans 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
      }
    }
   .....
}

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

Parlons du problème de mémoire de keepalive dans Vue

nice~~

总结

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

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

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

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

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

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

adresse d'émissiongithub.com/vuejs/vue/i…

(Partage de vidéos d'apprentissage : développement web front-end, Vidéo de programmation de base)

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer