【Recommandation associée : tutoriel vidéo vuejs】
Récemment, il a été découvert que les projets de l'entreprise tombaient parfois en panne.
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
2 Localisation du problème
En 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.
É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: '' } }, 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> .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>
<template>
<div>组件view<input> </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>
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 viasur 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.
x
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.结果问题依然存在,于是又试了下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
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,找到对应的源码。
在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]) } }</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依然被保留着。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 } } ..... }
再次怀着喜悦的心情运行,发现这次靠谱了。
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!