ホームページ  >  記事  >  ウェブフロントエンド  >  Vue のキープアライブのメモリ問題について話しましょう

Vue のキープアライブのメモリ問題について話しましょう

青灯夜游
青灯夜游転載
2022-10-14 20:21:542856ブラウズ

Vue のキープアライブのメモリ問題について話しましょう

[関連する推奨事項: vuejs ビデオ チュートリアル]

1. 原因

最近発見したのは、 company プロジェクトは時々クラッシュします。

Vue のキープアライブのメモリ問題について話しましょう

#最初はコードに無限ループが書かれているのかと思いましたが、調べてみると見つかりませんでした。

その後、パフォーマンス検査を行った結果、メモリが 1 G を超えていることが判明しました。メモリが正常に再利用されず、マルチページからシングルページに統合された後にプロジェクトが発生した可能性があります。単一ページではキープアライブ内部タブを使用します。したがって、最初の推論は、メモリが過密である可能性があるということです。

#原因の特定

#パフォーマンスを通じて現在のメモリ使用量を表示 -> メモリ,

  • 内部タブを必死に開けたり閉じたりしていると、メモリが驚くべき 2g に達し、関連する操作が応答しなくなり始め、ページがスタックしたり、画面が真っ白になったりすることがわかりました。

Vue のキープアライブのメモリ問題について話しましょう

    #コマンドを実行すると、2g が使用可能、2g が使用済み、上限が 4g であることがわかります

Vue のキープアライブのメモリ問題について話しましょう

# #驚くべきことに、2g を超えても、しばらく経っても引き続き操作してプレイすることができ、メモリは 4g に達しています。

Vue のキープアライブのメモリ問題について話しましょう

現時点では、Barbie Q はすでに進行中です。console.log で Enter キーを押した後、実行して出力するスペースはありません。

#2. 問題の特定Vue のキープアライブのメモリ問題について話しましょう

#1. 復元シナリオ

#内部システム コードの複雑さ、交差するロジックと隠れたメモリ リーク コードが原因です。同社の他の組み込みマルチタブ キャッシュ プロジェクトと比較しても、同様の問題が存在します。したがって、純粋な環境を構築し、段階的に根本から分析する必要があります。 まず、プロジェクトで使用されているバージョン環境を復元します。

2. デモを作成します

まず、問題を再現するデモを作成します。 vue-cli を使用して、バージョン 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

デモ効果

##[追加] をクリックすると、80mb のコンポーネントが作成されます。4 つの新しいコンポーネントが追加されたことがわかります。キープアライブは約 330mb を占有します。(リアルタイムのモニタリングとパフォーマンス インターフェイスの計算、メモリ診断レポートは偏ります)Vue のキープアライブのメモリ問題について話しましょう

  • [削除] をクリックすると、デフォルトで最後の要素が削除されます。要素の #xx

    を使用して削除することもできます。削除するたびに、最初にホームページに戻ります。削除されたビューが表示されているビューではないことを確認してください。

  • 3. 問題を再現します

1. 4 つのコンポーネントを作成し、最後の a4 を削除した後、同時にメモリはすぐにリサイクルされ、メモリは解放されません。まだ328mbです。 2. しかし、a3 がもう 1 つ削除されると、80 が解放され、人々はさらに混乱します。

3. それだけではありません。新しいものを 4 つ追加し、最初のものを削除すると、リアルタイムでリリースできます。

Vue のキープアライブのメモリ問題について話しましょう いいやつです、vue の公式APIもそんなに信頼できないのですか?プログラマーにとって、不確実な問題は実際のエラーよりもはるかに困難です。 Vue のキープアライブのメモリ問題について話しましょう

早速公式サイトを確認したところ、2.6.12 から 2.7.10 までの間の 2.6.13 でキープアライブの問題が vue2 で修正されていたことがわかりました。2.7.10 では ts を使用して書き換えられ、vue3 が導入されたため、安定性は、2.6 の最新の 2.6.14 にのみアップグレードされます。

Vue のキープアライブのメモリ問題について話しましょう

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

4.分析

4.1全局引用是否正常释放

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

Vue のキープアライブのメモリ問題について話しましょう

Vue のキープアライブのメモリ問題について話しましょう

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

1Vue のキープアライブのメモリ問題について話しましょう

4.2keepalive 的cache是否正常释放

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

1Vue のキープアライブのメモリ問題について話しましょう

4.3挨个组件检查引用关系

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

1Vue のキープアライブのメモリ問題について話しましょう

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

1Vue のキープアライブのメモリ問題について話しましょう

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

1Vue のキープアライブのメモリ問題について話しましょう

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

1Vue のキープアライブのメモリ問題について話しましょう

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

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。

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

Vue のキープアライブのメモリ問題について話しましょう

通过live server启动服务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,找到对应的源码。

Vue のキープアライブのメモリ問題について話しましょう

在render方法里打上断点,可以发现每当路由发送变化,keepalive的render方法都会重新渲染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依然被保留着。NNDVue のキープアライブのメモリ問題について話しましょう点击后发现,是a4的dom已经没在显示,dom处于游离detach状态,看看是谁还引用着。好家伙,又是父节点keepalive的引用着,这次是elm。

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
      }
    }
   .....
}

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

Vue のキープアライブのメモリ問題について話しましょう

nice~~

总结

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

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

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

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

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

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

問題アドレスgithub.com/vuejs/vue/i…

(学習ビデオ共有: Webフロントエンド開発, プログラミングの基礎ビデオ)

以上がVue のキープアライブのメモリ問題について話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。