[vuejs/vue]希望keep-alive能增加可以动态删除已缓存组件的功能

2023-12-09 390 views
2

What problem does this feature solve?

我在使用vue做单页应用时,在多个功能切换的时候,希望能达到动态创建tab进行切换的效果,和以前使用iframe一样,这些tab是keep-alive的,但如果太多keep-alive的时候,浏览器内存占用会过大。我希望能够达到可切换的tab最多例如只有十个,前面的我会用程序自动关闭对应tab,此时希望能把其缓存的组件也对应清除。

What does the proposed API look like?

例如:vm.$clearKeepAlived(routerName)

回答

6

@jkzing 非常感谢,我上面说的场景您的方法应该可以完成,但我还有个场景,例如列表页面,如果是从tab切换过来的时候,希望是keep-alive,直接从内存拿数据,如果是修改一条数据后,程序跳转到列表页面,希望是能够后台获取新的数据,虽然我可以页面完成后再从后台获取,但感觉不够优雅,如果有上述api,我只需要在修改后把这个router的keep-alive清除,数据应该就会自动从后台获取了。不知道我表述的是否能够让您明白,总之,我希望能够优雅的把单页应用做成和以前iframe的多tab效果一样: http://www.sucaihuo.com/modals/16/1653/demo/ 以上,谢谢。

5

@okjesse 这个也是可以做到的,你需要的只是把include或者exclude提升到更上层组件;或者,如果用了vuex的话把include放到vuex store中。

总之,vue提供了基础的API来完成这件事,想方便地用起来的话需要稍微封装下。

5

@jkzing 明白,谢谢了,我现在是自己写了个keep-alive实现的,我试试 按照您的方法

9

@jkzing ,不过就算这样能够实现,我觉得最好还是得有个这个api,语义会不太一样,我所希望的是清除已经keep-alive的数据,组件本身还是alive的,而不是不停的切换是否会alive

0

这只是其中一个问题,我们遇到的不停开新页面,会导致内存过大,最后浏览器卡死的问题,应该和keepalive无关。如果可以,我们重现个现象给@jkzing看

8

我们要实现的是,在跳转到页面的前一刻才决定是否用缓存的页面,还是新页面。老的api做不到的吧

7

我目前也遇到了这个需求,再菜单列表点击连接,在tabs里面增加一个标签并缓存新开的组件;点击关闭标签我希望能删除缓存的组件;我是在组件里面deactivated钩子函数调用this.$destroy();这样能清除缓存,但是发现下次打开这个连接的时候,新的组件不会被缓存了

6

@fadexiii 现有的方法我解决了,通过动态设置includes,可以实现

8

@okjesse 我的keep-alive 里面包含的是一个<router-view></router-view>用includes的方法不起作用

4

@okjesse 刚刚是我没有定义conponent的name属性,定义之后可以了,多谢

2

FYI: implemented in 2cba6d4cb1db8273ee45cccb8e50ebd87191244e

4

@fadexiii #6961 和你一样的需求,请问下你们怎么解决的呀

8

@Jack-93 利用keep-alive的include,新打开标签时,把当前组件名加入到include数组里,关闭标签时从数组中删除关闭标签的组件名就可以了

1

看到$destroy这个方法,我觉得这就是正确的用法,通过组件里不同的用户行为决定离开页面时是否缓存组件。 (比如一个留言组件,留言提交成功后这个组件应该销毁,如果没有提交,则应该保留缓存,让用户下次进来继续。) 然而,destroy之后,再次重复进入这个组件会出现奇怪的结果,组件不能再被缓存了,我觉得这是bug吧。

9

@wanyaxing 我也遇到过你类似的问题 我用了keep-alive 缓存所有的子组件,列表页A---》详情页B---》三级页面D, 在正向前进的时候,我将打开的路由全部存在sessionStorage里面,返回的时候判断是下一个路由是否在sessionStorage里面,如果在的话,就将当前实例销毁。 这样的话,第一次没问题,能正常运行,两次或者多次以后,就会出现会B被缓存多次的情况,每次从D返回至B的时候,会生成一个新的B,而之前缓存的B还一直在内存当中,如图所示: image

核心代码如下: image

6

@realfly08 我尝试提交了一个PR去解决这个问题,不过好像提交失败了,我没搞清楚怎么通过vue的自动代码审查-。- https://github.com/vuejs/vue/pull/7151

我之前也是准备全站缓存,然后碰到了缓存更新的逻辑大坑。 现在我放弃了全站缓存,只在关键的几个表单页开启了缓存,然后在代码里动态判断是否保留当页数据,算是临时跳过了这些问题,然而我自己还是感觉这个逻辑不够爽利。

7

@wanyaxing 我也遇到了类似的问题,要实现的逻辑功能,A->B->C 3个页面, 返回的时候使用缓存,再次进入的时候重新渲染,目前是每个页面都keep alive, 然后我在B页面进行判断如果是返回上一级页面(go -1)就在deactivated中销毁当前组件,调用$destroy,当按顺序再次进入到C页面,返回到B页面,可以看到又重新创建了B页面,同时缓存中还存在着一个B页面

1

@fadexiii keep-alive 配合router-view怎么用?

  <keep-alive :include="includes">
     <router-view></router-view>
  </keep-alive>

includes 写的是路由配置中的name: 'login'

const router = new Router({
  routes: [
    {
      path: '/login',
      name: 'login',
      component: Login
    }]
 })

为什么我把 includes 赋值为空字符串,依然可以缓存?我的需求就是需要手动销毁缓存的路由

7

在组件被销毁之后,缓存并没有被清空掉,我觉得问题是在这里。 项目负责人推荐使用 include 这样的属性来处理,我觉得并没有直接destroy 来的直接吧,内存还能有所改善

@wanyaxing 看了你提交的PR ,应该是有效的,或者开放一个 remove cache 的API 也行的吧

8

@leixu2txtek 我已经放弃了那个PR,通过强行清除缓存的方法,我变相得实现了动态删除缓存组件的功能。

我目前全站使用缓存,通过拦截页面离开的路由事件来根据业务逻辑实现删除缓存的功能,以下代码片段供参考:

  • 将router-view放到keep-alive中,默认全站默认使用缓存。 <keep-alive><router-view class="transit-view"></router-view></keep-alive>
  • 我在routes里将所有的页面进行了分层。如meta.rank代表页面层次,如1.5>2.5>3.5意味着从第一层进入第二层进入第三层页面。
    routes: [
    {   path: '/', redirect:'/yingshou', },
    {   path: '/yingshou',                meta:{rank:1.5,isShowFooter:true},          },
    {   path: '/contract_list',           meta:{rank:1.5,isShowFooter:true},          },
    {   path: '/customer',                meta:{rank:1.5,isShowFooter:true},          },
    {   path: '/wode',                    meta:{rank:1.5,isShowFooter:true},          },
    {   path: '/yingfu',                  meta:{rank:1.5,isShowFooter:true},          },
    {   path: '/yingfu/pact_list',        meta:{rank:2.5},                            },
    {   path: '/yingfu/pact_detail',      meta:{rank:3.5},                            },
    {   path: '/yingfu/expend_view',      meta:{rank:4.5},                            },
    {   path: '/yingfu/jizhichu',         meta:{rank:5.5},                            },
    {   path: '/yingfu/select_pact',      meta:{rank:6.5},                            },
    {   path: '/yingfu/jiyingfu',         meta:{rank:7.5},                            },
    ]
  • 因为所有页面都会缓存,所以核心思路是【何时销毁缓存?】。我的设计是:同层级页面切换或进入下一层页面都会保留当前页缓存,【返回上一层页面时则销毁当前页面缓存】。
  • 所以我在main.js里,使用Vue.mixin的方法拦截了路由离开事件,并在该拦截方法中实现了销毁页面缓存的功能。核心代码如下:

Vue.mixin({ beforeRouteLeave:function(to, from, next){ if (from && from.meta.rank && to.meta.rank && from.meta.rank>to.meta.rank) {//如果返回上一层,则摧毁本层缓存。 if (this.$vnode && this.$vnode.data.keepAlive) { if (this.$vnode.parent && this.$vnode.parent.componentInstance && this.$vnode.parent.componentInstance.cache) { if (this.$vnode.componentOptions) { var key = this.$vnode.key == null ? this.$vnode.componentOptions.Ctor.cid + (this.$vnode.componentOptions.tag ? ::${this.$vnode.componentOptions.tag} : '') : this.$vnode.key; var cache = this.$vnode.parent.componentInstance.cache; var keys = this.$vnode.parent.componentInstance.keys; if (cache[key]) { if (keys.length) { var index = keys.indexOf(key); if (index > -1) { keys.splice(index, 1); } } delete cache[key]; } } } } this.$destroy(); } next(); }, });



总结:其实就是通过页面组件所在的上层keepAlive组件,暴力操控该对象中的cache列表。。。

简单又直接,虽然不是很优雅,然而很好用,哈哈:)

当然还是希望官方能支持API出来更好。
5

@wanyaxing 我是通过路由来控制页面的缓存

比如,我有 /executed/,以及他的子路由 /executed/chart/ ,我同样是用 mixin 方法hack 路由离开时候 销毁不需要缓存的组件,我要做到的是从父级到子集路由,父级被缓存,其他情况都被销毁,这样我们只要控制路由就行了,不用增加任何的属性什么的,我比较懒;

// 支持页面保持
Vue.mixin({
  beforeRouteLeave(to, from, next) {

    if (!to.path.startsWith(from.path)) this.$destroy();

    next();

  }
});

@wanyaxing 谢谢,你的代码对我非常有用。我参考一下;

我发现确实是缓存没有被销毁导致的。尽力在和 @LinusBorg 沟通

7

@wanyaxing 我一直在找拿到 cache 的方法,对vue核心代码不太熟悉,你的代码比较有用,我测试通过了,非常棒,暂时这么做,希望官方可以解决这个问题吧,我问了不少人也查了很多资料,都有这样或者那样的问题

4

哈哈。 @leixu2txtek 在这个基础上,推荐你一个插件,搭配 https://github.com/wanyaxing/vue-router-then 组合使用,效果更好。 使用 vue-router-then 在当前页面打开子页面,可以在当前页面拿到子页面的信息或事件。 this.$routerThen.push('/chose').then(vm => {})

举一个最常用的例子,比如在填写订单页面需要打开地址列表进行选择,

<input v-model="price" v-model-link="'/select_address'" />

然后在/select_address打开的路由里,选择地址后,触发一个input事件并后退页面即可。

<script>
methods:{
    clickSomeOne:function(value){
        this.$emit('input',value);
        this.$router.go(-1);
    },
}
</script>

这需要在前文提到的缓存策略的基础上才能实现父子路由之间的直接互动,然而实现之后会非常有意思,开发起来思路很直观,不用考虑在vuex里存储临时数据,直接开页面拿数据即可。

8

@fadexiii 你的意思是 如果使用的是router,那么include 所包含的 应该是 对应router的component中的name吗? ---- router.js ----

import ComponentA from 'views/a
export default [
  {path: 'a', component: ComponentA, name: a}
]

---- views/a.vue ----

export default {
  name: 'xxxa'
}

include 中应该存的是 ComponentA or a or xxxa ?

6

@jkzing 如果我同一个组件加载了两次,一个需要缓存,一个不需要缓存怎么弄,他们的组件name是一样的呀

3

我也遇到这个问题了 在router入口加上 meta: { keepAlive: true // 需要被缓存 } 点击关闭的时候设置为false 接着用监听器 改为true 具体看https://mp.csdn.net/postedit/82628953 现在测试还没问题 不知道 后面会不会出现问题

0

tab切换的时候缓存 关闭缓存要销毁及下次打开重新加载 多重嵌套子路由页面也是

7

这个例子,我看懂了。我想在这里请教下大神,我的业务是这样的,左侧是纵向的一个tabs,这个tabs是用户可以动态添加或者删除的,点击tabs中的任意一个,右侧显示和这个tab对应的一棵tree,其实是同样一棵树,只是树选中的节点不一样。最开始的想法就是添加一个tab项,右侧添加一个tree,切换tab时,tree进行相应的隐藏显示(v-show),但是这样tab比较多的时候,右侧tree也会很多,dom节点太多,如果不用隐藏显示,用v-if的话,又会存在tree的状态(选中、展开、滚动)无法保存,所以我后来想到了keep-alive,他是可以保存状态的。但是我是vue的单文件组件,tabs在主页面,tree是一个组件被import进去的,import进来后,直接在components属性里注册了,怎么才能像你发的这样,给tree创建多个名字呢?但其实我就一个组件,创建名字只是为了和左侧tab进行关联,更确切的说,是为了在左侧删除一个tab项时,也在keepalive中删除这个tab项对应的tree缓冲?可是要怎么做呢?

2

但是这样内存还是不会释放,我关掉keepalive就没事,加上keepalive然后手动移除缓存并销毁内存还是一直在上涨,不知道有没有关注这方面?

2

@Liudapeng 我之前没有仔细研究过内存使用,我曾粗略做过测试,在内存图时间线中看到有内存掉落后就没有细究。

最近有点忙,前两天就看到你的留言了,一直没时间给你回复。

我刚才又做了次测试,重复几次深入五六层页面然后返回到首页,内存时间线结果如下:

  • 开启 keep-alive,并动态销毁组件(即使用前文我提到的方法):

    可以明显的看到时间线里有内存掉落的情况,我觉得内存是有销毁的。 snipaste_2018-09-29_17-53-09

不使用 keep-alive:

但总体而言,我觉得使用动态销毁之后,内存的使用还在可接受范围之内,所以如果你出现了内存大幅上涨的情况,应该检查是否在组件销毁时没有销毁绑定事件等原因(导致组件虽然从 keepAlive 的cache 数组里移除了,但并没有从内存销毁)。

请再检查。

8

@bigggge name是 组件 内部的属性 不是路由的属性

7

我的问题更奇葩。我的vue版本是2.3.3,A页面有个滚动加载下一页的代码,我从A页面切换到B页面,在B页面滚动时发现,A页面的加载下一页的代码还会执行。当我把全局 keep-alive 去掉后,就不会出现上述问题了。但我所有的 activated 又失效了,得用 created。

5

有可能是全局监听导致的 可以在滚动时判断是否为当前路由 再决定要不要加载下一页

0

是的,我用的是 vue-infinite-scroll 插件,当我使用 全局 keep-alive 时,它没移除掉监听事件,我已经重写这个插件了。

7

我也是这个疑问,include无法满足这个需求,多个路由页面对应一个组建,那就只有一个组件name,咋办?

1

@heng1234 你好你这个问题解决了吗,哪位这个问题可以留个联系方式指导一下吗

5

不要用路由切换页面 用组件的方式切换 这篇博客仅供参考 我是用组件切换的方式的 不是用这种 https://blog.csdn.net/qq_39313596/article/details/82628953

xiaoluoheng@foxmail.com

发件人: Codezero123 发送时间: 2019-12-03 10:04 收件人: vuejs/vue 抄送: hlvy; Mention 主题: Re: [vuejs/vue] 希望keep-alive能增加可以动态删除已缓存组件的功能 (#6509) tab切换的时候缓存 关闭缓存要销毁及下次打开重新加载 多重嵌套子路由页面也是 @heng1234 你好你这个问题解决了吗 — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

4

@wanyaxing 你这种对于三级路由缓存不起效,比如vue-element-admin里面,开启多个三级tag标签的时候,切换tag标签不会销毁缓存组件,但是只开启一个三级路由tag标签,是可以销毁的。比如开启下面的menu1-0 和menu1-1。 路由。 { path: '/nested', component: Layout, redirect: '/nested/menu1/menu1-1', name: 'Nested', meta: { title: 'Nested Routes', icon: 'nested' }, children: [ { path: 'menu1', component: () => import('@/views/nested/menu1/index'), // Parent router-view name: 'Menu1', meta: { title: 'Menu 1' }, redirect: '/nested/menu1/menu1-1', children: [ { path: 'menu1-0', component: () => import('@/views/nested/menu1/menu1-0'), name: 'Menu1-0', meta: { title: 'Menu 1-0', noCache: true } }, { path: 'menu1-1', component: () => import('@/views/nested/menu1/menu1-1'), name: 'Menu1-1', meta: { title: 'Menu 1-1' } }, { path: 'menu1-2', component: () => import('@/views/nested/menu1/menu1-2'), name: 'Menu1-2', redirect: '/nested/menu1/menu1-2/menu1-2-1', meta: { title: 'Menu 1-2' }, children: [ { path: 'menu1-2-1', component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), name: 'Menu1-2-1', meta: { title: 'Menu 1-2-1' } }, { path: 'menu1-2-2', component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'), name: 'Menu1-2-2', meta: { title: 'Menu 1-2-2' } } ] }, { path: 'menu1-3', component: () => import('@/views/nested/menu1/menu1-3'), name: 'Menu1-3', meta: { title: 'Menu 1-3' } } ] } ] }

`Vue.mixin({ beforeRouteLeave:function(to, from, next){ if (from.meta.noCache) { if (this.$vnode && this.$vnode.data.keepAlive) { if (this.$vnode.parent && this.$vnode.parent.componentInstance && this.$vnode.parent.componentInstance.cache) {

      if (this.$vnode.componentOptions) {
        var key = this.$vnode.key == null
          ? this.$vnode.componentOptions.Ctor.cid + (this.$vnode.componentOptions.tag ? `::${this.$vnode.componentOptions.tag}` : '')
          : this.$vnode.key;
        var cache = this.$vnode.parent.componentInstance.cache;
        var keys = this.$vnode.parent.componentInstance.keys;
        if (cache[key]) {
          if (keys.length) {
            var index = keys.indexOf(key);
            if (index > -1) {
              keys.splice(index, 1);
            }
          }
          delete cache[key];
        }
      }
    }
    this.$destroy();
  }
}
next();

}, });`

7

思路就是这么个思路,思路是没有问题的,具体何时调用何时销毁缓存,得看你业务中具体实现。

3

我们业务这边这种场景应该很场景吧: 1、浏览了几个页面,需要缓存 2、点击了退出,需要清除所有的缓存 3、登录后有其他报错跳转提示,这些页面都不做缓存 4、登录成功,新的页面继续缓存

现在发现到2的时候无法清除所有缓存,把keep-alive的max值放到了vuex并设置为了1还是无效,看vue开发者工具里还是有缓存页面的。 我这边还有更奇怪的是,在3里其他非登录页面跳转,会触发1里面的缓存页面的mounted,而且看开发者工具,这些页面都是inactive的,哎,这导致页面逻辑各种问题。 找了好久,还是不知道为啥

9

你这种场景里啊,在2的时候,应该考虑直接刷新页面,是最保险也最直接的方案。

4

因为includes对应的是组件的name,如果多个路由对应一个组件,怎么办呢?有什么办法可以给同一个组件命不同的名字吗?

3

目前来看这是最好的解决方法. 尤其是需要打开多个同组件不同参数的标签页时.

2

我这里的场景是点击导航的时候不缓存,但是页面提供了一个返回上级页面,这时取缓存;且详情页返回到列表页也取缓存,目前是实现了需求。

const cacheThis = new Map();
Vue.mixin({
    async beforeRouteLeave(to, from, next) {
        const isNavClick = (await sessionStorage.getItem('isNavClick')) === '1'
        if (isNavClick) {
            if (cacheThis.has(to.name)) {
                cacheThis.get(to.name).$destroy()
            } else {
                cacheThis.set(from.name, this)
            }
        }
        sessionStorage.setItem('isNavClick', '0');
        next()
    }
})
9

讨论了快4年了,这个问题还没解决啊???比较明显的一个问题是 keep-alive 应该缺少两个必不可少的配套方法:clear 方法用来清空 keep-alive 实例所属的所有已缓存组件或页面;remove() 方法用来移除 keep-alive 实例所属的某个已缓存组件或页面;

0

暴力操作 keys 和 cache,并用 $destroy 销毁实例,很好用。

1

希望官方打开并解决,确实有问题! “多个路由对应一个组件” 这不是include能解决的事

5

In this way ! @elewen @zero7u

<!-- Tabs.vue -->
<template>
  <router-link>
     Tab A
     <span @click.prevent.stop="closeSelectedTag(tag)" />
   </router-link>
</template>
<script>
export default {
  computed: {
    cache: {
      get() {
        if (!this.$route.matched[1]) return 
        const instances = this.$route.matched[1].instances;
        return instances.default.$vnode.parent.componentInstance.cache;
      },
      set(val) {
        this.$route.matched[1].instances.default.$vnode.parent.componentInstance.cache = val
      }
    }
  },
  methods: {
    closeSelectedTag(view) {
      // Remove selected tag 
      // ...
      // Remove the cache from cache
      const cache = this.cache;
      const str = RegExp('.*' + view.fullPath);
      let key = '';
      Object.keys(cache).forEach(el => {
        if (str.test(el)) {
          key = el;
        }
      });
      delete cache[key];
    }
  }
}
</script>
9

b

4

@zhangchugao 你好,你发的链接好像失效了,能再贴一下吗,谢谢

2

@zhangchugao 你好,你的解决方案非常好,和我的问题一模一样。但是有个小小问题,路由名字也是一样的。路由地址也是一样的,但是参数不一样,两个页面的,分别缓存起来。

例如: http://localhost/#/ehr/parameter/index/3 http://localhost/#/ehr/parameter/index/13

路由信息:

  {
    path: "/ehr/parameter",
    component: Layout,
    hidden: true,
    children: [
      {
        path: "index/:wid(\\d+)",
        component: resolve =>
          require(["@/views/datamodel/winparameter/index"], resolve),
        name: "WinParameter",
        meta: { title: "加载参数", activeMenu: "/datamodel/winparameter" }
      }
    ]
  }

标签卡的组件页面,全部都空白页面了 image

9

你好,我这边测试了,发现没有问题,请问你是否修改了自定义keep-alive组件js的缓存key从name修改为更具path路径缓存,原作者贴的代码当中使用的组件的name来缓存对应的每个路由的。我修改如下图所示: image 你可以查看你是否修改了图片中的代码。 1652163725 1652163725(1)

0

是的,我刚刚改好了,vue2测试了,有vue3版本的吗?官方一直不处理这个问题,郁闷。

4

真的服了,到现在还不能手动清keepalive?vue3又把$destroy去了,嘛呢,开倒车呢?