Rumah > hujung hadapan web > View.js > teks badan

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

青灯夜游
Lepaskan: 2022-10-14 20:21:54
ke hadapan
2893 orang telah melayarinya

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

[Cadangan berkaitan: tutorial video vuejs]

1 Punca

Baru-baru ini menemui Projek syarikat kadangkala ranap.

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

Pada mulanya saya fikir ia adalah beberapa gelung tak terhingga yang ditulis dalam kod, tetapi saya tidak menjumpainya selepas menyemak.

Kemudian, melalui pemeriksaan prestasi, didapati memori mencecah lebih daripada 1 G. Mungkin ingatan itu tidak dikitar semula secara normal, dan projek itu berlaku selepas menyepadukan dari berbilang halaman ke halaman tunggal halaman tunggal menggunakan keepalive tab dalaman accomplish. Jadi kesimpulan awal ialah ingatan mungkin terlalu sesak.

Sebab lokasi

Lihat penggunaan memori semasa melalui prestasi -> Dengan membuka dan menutup tab dalaman secara tergesa-gesa, saya mendapati bahawa memori telah mencapai 2g yang menakjubkan, operasi yang berkaitan telah mula menjadi tidak responsif, dan halaman tersekat atau malah skrin kosong

Mari kita bincangkan tentang masalah ingatan keepalive dalam VueAnda boleh melihat melalui arahan bahawa 2g boleh digunakan, 2g telah digunakan, dan penutupnya ialah 4g

Mari kita bincangkan tentang masalah ingatan keepalive dalam VueYang menakjubkan selepas 2g, anda masih boleh terus beroperasi selepas beberapa ketika Jika anda terus bermain, memori telah mencapai 4g

Mari kita bincangkan tentang masalah ingatan keepalive dalam VuePada masa ini, Barbie Q sudah ada selepas menekan Enter dalam console.log, tiada ruang untuk melaksanakan dan mengeluarkan

2. Mengesan masalahMari kita bincangkan tentang masalah ingatan keepalive dalam Vue

1 >

Disebabkan oleh kerumitan kod sistem dalaman dan kod dengan logik bersilang dan kebocoran memori tersembunyi . Membandingkan projek caching berbilang tab terbina dalam syarikat yang lain, masalah serupa juga wujud. Oleh itu, adalah perlu untuk membina persekitaran yang murni dan menganalisisnya dari bawah langkah demi langkah. Mula-mula memulihkan persekitaran versi yang digunakan oleh projek. 2. Tulis demo

Mula-mula tulis demo untuk menghasilkan semula masalah. Gunakan vue-cli untuk mencipta projek yang sepadan dengan versi vue2.6.12, vue-router3.6.4main.js

App.vue

view/index .vue

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')
Salin selepas log masuk

view/a.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>
Salin selepas log masuk

router/index.js

<template>
    <div>首页</div>
</template>
<script>
export default {
    name:&#39;index&#39;,
}
</script>
Salin selepas log masuk

kesan demo

<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>
Salin selepas log masuk

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
Salin selepas log masuk

Mengklik Tambah akan menghasilkan komponen 80mb Anda boleh melihat bahawa 4 komponen baharu telah ditambah dengan kira-kira 330mb (pemantauan masa nyata dan pengiraan antara muka. laporan diagnosis ingatan akan menjadi Sisihan)

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

    Mengklik padam akan mengalih keluar elemen terakhir secara lalai Anda juga boleh memadamkannya melalui
  • setiap kali anda memadamkannya lompat kembali ke laman utama dahulu untuk memastikan paparan yang dipadam Bukan paparan yang dipaparkan.

  • x3. Menghasilkan semula masalah

1. Selepas mencipta 4 komponen dan memadam a4 yang terakhir, Pada masa yang sama masa, ingatan segera dikitar semula dan ingatan tidak dilepaskan. Masih 328mb. 2 Tetapi apabila satu lagi a3 dipadamkan, 80 sebenarnya dikeluarkan, yang membuatkan orang ramai semakin keliru.

3. Bukan itu sahaja jika saya menambah 4 yang baharu dan kemudian memadamkan yang pertama, ia boleh dikeluarkan dalam masa nyata

Mari kita bincangkan tentang masalah ingatan keepalive dalam VueOrang baik, pegawai Vue API juga Jadi tidak boleh dipercayai? Bagi pengaturcara, masalah yang tidak pasti adalah lebih sukar daripada ralat sebenar. Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

Cepat dan semak tapak web rasmi dan dapatkan vue2 membetulkan masalah tentang keepalive pada 2.6.13 dari 2.6.12 hingga 2.7.10 Sejak 2.7.10 ditulis semula menggunakan ts, dan vue3 telah diperkenalkan The compositionAPI , untuk kestabilan, hanya dinaik taraf kepada 2.6.14 terkini daripada 2.6.

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

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

4.分析

4.1全局引用是否正常释放

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

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

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

1Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

4.2keepalive 的cache是否正常释放

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

1Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

4.3挨个组件检查引用关系

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

1Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

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

1Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

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

1Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

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

1Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

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

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

根据一层层关系最后发现

 a3组件.$vnode.parent.componentOptions.children[0] 引用着 a4
Salin selepas log masuk

导致a4 无法正常释放

基于这个点,查询了前面a2,a3 也存在引用的关系,a1 正常无人引用它。

a2组件.$vnode.parent.componentOptions.children[0] 引用着 a3
a1组件.$vnode.parent.componentOptions.children[0] 引用着 a2
a1组件 正常,没被引用
Salin selepas log masuk
  • 这里看到看出 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。

Mari kita bincangkan tentang masalah ingatan keepalive dalam 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 //拉取代码
Salin selepas log masuk

修改package.json,添加 --sourcemap

"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev",
Salin selepas log masuk

本地运行

npm run dev
Salin selepas log masuk

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

通过live server启动服务Mari kita bincangkan tentang masalah ingatan keepalive dalam 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
  };
Salin selepas log masuk

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 -->
  
Salin selepas log masuk

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

4.调试代码

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

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

在render方法里打上断点,可以发现每当路由发送变化,keepalive的render方法都会重新渲染Mari kita bincangkan tentang masalah ingatan keepalive dalam 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>
Salin selepas log masuk

这里包含了整个keepalive的所有逻辑,

  • 刚开始也以为是LRU的设置问题,测试后发现keepalive的数组都是能正常释放。

  • 怀疑是max最大长度限制,解决也是正常。 确保keepalive内部能正常释放引用后,就要想如何修复这个bug,关键就是把children设置为空

组件.$vnode.parent.componentOptions.children = []
Salin selepas log masuk

最合适的位置就在每次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 = []
      }
    }
   .....
}
Salin selepas log masuk

怀着喜悦的心情以为一切ok,运行后发现,a4依然被保留着。NNDMari kita bincangkan tentang masalah ingatan keepalive dalam Vue点击后发现,是a4的dom已经没在显示,dom处于游离detach状态,看看是谁还引用着。好家伙,又是父节点keepalive的引用着,这次是elm。

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue于是在keepalive源码的render方法加入

entry.componentInstance.$vnode.parent.elm = null
Salin selepas log masuk

整体代码如下

  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
      }
    }
   .....
}
Salin selepas log masuk

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

Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue

nice~~

总结

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

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

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

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

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

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

alamat isugithub.com/vuejs/vue/i…

(Mempelajari perkongsian video: pembangunan bahagian hadapan web, asas pengaturcaraan Video)

Atas ialah kandungan terperinci Mari kita bincangkan tentang masalah ingatan keepalive dalam Vue. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:juejin.cn
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!