如何管理Vue中的缓存页面


Posted in Vue.js onFebruary 06, 2021
<keep-alive>
 <router-view />
</keep-alive>

Vue中内置的<keep-alive>组件可以帮助我们在开发SPA应用时,通过把全部路由页面进行缓存(当然也可以有针对性的缓存部分页面),显著提高页面二次访问速度,但是也给我们在某些场景带来了困扰,其中包含两个主要矛盾:

  1. 缓存页面如何在合适的时机被销毁 (keep-alive组件提供了三个参数来动态配置缓存状态,但是作用有限,后面分析)
  2. 同一个路径如何缓存多个不同的页面(同页不同参),比如淘宝商品页面继续跳转另一个商品页面

本文主要围绕这两个问题探讨,后文用问题一和问题二指代。

本文默认所有页面都是keep-alive

问题一 销毁

当随着业务逻辑变得复杂,路由栈也逐渐升高,理论上用户可以无限的路由下去,不可避免的我们需要管理这些缓存在内存中的页面数据,页面数据包含两部分,Vue实例和对应的Vnode。查看 Vue 源码中src/core/components/keep-alive.js关于缓存的定义

this.cache = Object.create(null) //用来缓存vnode cache[key] => Vnode
 this.keys = [] //用来记录已缓存的vnode的key

缓存后并不会重用 Vnode,而是只用它上面挂载的 Vue 实例。

if (cache[key]) {
 vnode.componentInstance = cache[key].componentInstance //仅从缓存的vnode中获取vue实例挂在到新的vnode上
 // make current key freshest
 remove(keys, key)
 keys.push(key)
}

为什么不用呢,因为有BUG,最早一版实现里确实是会直接使用缓存的 Vnode。

出自src/core/components/keep-alive.js init version

export default {
 created () {
 this.cache = Object.create(null)
 },
 render () {
 const childNode = this.$slots.default[0]
 const cid = childNode.componentOptions.Ctor.cid
 if (this.cache[cid]) {
  const child = childNode.child = this.cache[cid].child //直接获取缓存的vnode
  childNode.elm = this.$el = child.$el
 } else {
  this.cache[cid] = childNode
 }
 childNode.data.keepAlive = true
 return childNode
 },
 beforeDestroy () {
 for (const key in this.cache) {
  this.cache[key].child.$destroy()
 }
 }
}

我们需要管理的其实就是cache和keys,keep-alive提供了三个参数来动态管理缓存:

include - 只有名称匹配的组件会被缓存。
exclude - 任何名称匹配的组件都不会被缓存。
max - 最多可以缓存多少组件实例。

它们的作用非常简单,源码写的也很简单易读:

所以当我们想要管理这些缓存时,简单的方案就是操作这三个参数,修改include和exclude来缓存或者清除某些缓存,但是需要注意的是它们匹配的是组件的name:

出自src/core/components/keep-alive.js

const name: ?string = getComponentName(componentOptions)

所以清除缓存是会无差别的把某个组件的所有实例全部清除,这显然不满足我们的需求。

max的逻辑则是超过最大值时清除栈底的缓存,

出自src/core/components/keep-alive.js:

if (this.max && keys.length > parseInt(this.max)) {
 pruneCacheEntry(cache, keys[0], keys, this._vnode)
}

我们要解决问题一,官方提供给的API走不通,我们只能自己来了,我们需要的是解决两个子问题:

  1. 什么时候销毁
  2. 怎么销毁

1. 怎么销毁

先看怎么销毁,如果想销毁一个实例很简单,可以直接用 this.$destroy(), 这样可以吗,不行,这样缓存cache和keys中依旧保留了原来的vnode和key,再次访问时就会出现问题,vnode一直被留存,但是它身上的实例已经被销毁了,这时候在vue的update过程中就会再去创建一个vue实例,也就是说只要某个keep-alive的页面调用过一次this.$destroy(),但是没有清理缓存数组,这个页面之后被重新渲染时就一定会重新创建一个实例,当然重新走全部的生命周期。现象最终就是这个页面就像是没有被缓存一样。

this.$destroy(); //不适合keep-alive组件

所以销毁需要同时清理掉缓存cache和keys,下面定义了一个同时清除缓存的$keepAliveDestroy方法:

const dtmp = Vue.prototype.$destroy;
 const f = function() {
 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 = !isDef(this.$vnode.key)
   ? 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];
   }
  }
  }
 }
 dtmp.apply(this, arguments);
 }
 Vue.prototype.$keepAliveDestroy = f;

2. 什么时候销毁

那么什么时候销毁呢,有两个触发时机:

  1. replace时,页面A --replace--> 页面B (清除页面A)
  2. route back时 ,页面A --push--> 页面B --back--> 页面A (清除页面B)

replace 比较简单,我们可以直接拦截router的replace方法,在该方法中清除掉当前页面。(这里也有例外,比如切换Tab时,最后再说)

我们具体来看看route back这种情况,如果说我们的页面上有一个返回键,那么在这里清除缓存是非常正确的时机,但是我们不能忽略浏览器自带的返回键和安卓机上的物理返回键,这种情况考虑进来以后,仅使用返回键的方案就不能满足了。

2.1 方案一 使用route.query 记录当前页面栈深度

每次push或者replace是都增加query上一个参数,来记录当前深度

this.$router.push({
 path:"/targer",
 query:{
 stackLevel:Number(this.$route.query.stackLevel) + 1 	
 }
})

这个方案有明显弊端,外部暴露一个参数是非常丑陋且危险的,用户可以随便修改,在进行网页推广时,业务去生产环境自己拷贝到的推广链接也可能带着一个奇怪的 https://xxx.com/foo?bar=123&stackLevel=13后缀。弃用

2.2 方案二 使用Vue实例自身记录当前栈深度

hack掉router的push和replace方法以后,每次跳转的时候都可以给目标页的vm挂载一个_stackLevel,这样就解决了方案一的问题,不暴露给用户,URL中不可见,也无法修改,但是我们不能忽视浏览器中另一个恶魔——刷新键,在刷新的时候URL不会变,但是vm实例就需要重新创建了,那么我们的栈深度标示也就丢失了。弃用

2.3 方案三 使用history.state记录栈深度

那么最终就是既可以对用户不可见,又可以在刷新的时候得以保存。那就是history.state了,所以我们需要做的就是把stack深度保存到history.state中,它能够完整的保存整个路由链条。

当我们获取到目标页面栈深度小于当前页面时,我们就可以销毁当前页面了。

if(target.stack < current.stack){
 current.$keepAliveDestroy();
}

问题二 同页不同参缓存多个实例

可以在源码中看到 src/core/components/keep-alive.js

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 {
 cache[key] = vnode
 keys.push(key)
 // prune oldest entry
 if (this.max && keys.length > parseInt(this.max)) {
  pruneCacheEntry(cache, keys[0], keys, this._vnode)
 }
 }

一个vnode如果没有key才会使用组件名,所以默认缓存中的key是组件名,如果组件相同时,我们在每个页面都有自己的key就可以解决这个问题了,如何实现每个页面拥有自己的key呢。有两个子问题:

  1. 如何做到唯一
  2. 如何把key赋值给页面的vnode

1. 如何做到唯一

1.1 时间戳、超大随机数

key = Date.now()

1.2 路由栈高度+路径名

key = vm._stack + router.currentRoute.path 这个方案利用当前的栈高度+路径名,为什么需要路径名呢,因为replace的时候栈高度不变,只是路径名变了。

2. 如何把key赋值给页面的vnode

目前有两个方案给vue-router当前的Vnode的key来赋值:

2.1 通过route.query动态绑定Key

这个方案实现比较简单

//绑定key
...
<router-view :key='$route.query.routerKey' />
...


//push时
this.$router.push({
 path:"/foo",
 query:{
 	routerKey: Date.now() //随机key
 }
})

这种方式用起来非常简单有效,但是缺点同样也是会暴露一个奇怪的参数在URL中

2.2 通过获取到Vnode直接赋值

在哪个阶段给Vnode的key赋值呢,答案显而易见,在keep-alive组件render函数进入前, src/core/components/keep-alive.js

...
 render () {
 const slot = this.$slots.default
 const vnode: VNode = getFirstComponentChild(slot)
...

我们可以hack掉keep-alive的render函数,然后在这之前先把slot里的第一个子节点拿到以后,给它的key进行赋值,然后再调用 keep-alive的render:

const tmp = vm.$options.render //vm is keep-alive component instance
vm.$options.render = function() {
 const slot = this.$slots.default;
 const vnode = getFirstComponentChild(slot) // vnode is a keep-alive-component-vnode
 if (historyShouldChange) {
 if (!isDef(vnode.key)) {
  if (isReplace) {
  vnode.key = genKey(router._stack)
  } else if (isPush()) {
  vnode.key = genKey(Number(router._stack) + 1)
  } else {
  vnode.key = genKey(Number(router._stack) - 1)
  }
 }
 } else {
 // when historyShouldChange is false should rerender only, should not create new vm ,use the same vnode.key issue#7
 vnode.key = genKey(router._stack)
 }
 return tmp.apply(this, arguments)
}

总结

通过以上对于问题的分析,我们就解决了自动管理缓存的核心难题。本文是对开源库 vue-router-keep-alive-helper 的一次总结,此库是款简单易用的keep-alive缓存自动化管理工具,从此告别Vue缓存管理难题。如果对你有用,感谢慷慨Star。

演示Demo Sample Code

Bilibili演示视频 感谢三连。

以上就是如何管理Vue中的缓存页面的详细内容,更多关于vue 缓存页面的资料请关注三水点靠木其它相关文章!

Vue.js 相关文章推荐
Vue 打包的静态文件不能直接运行的原因及解决办法
Nov 19 Vue.js
Vue——解决报错 Computed property &quot;****&quot; was assigned to but it has no setter.
Dec 19 Vue.js
vue 实现基础组件的自动化全局注册
Dec 25 Vue.js
Vue 简单实现前端权限控制的示例
Dec 25 Vue.js
利用Vue实现简易播放器的完整代码
Dec 30 Vue.js
一文带你理解vue创建一个后台管理系统流程(Vue+Element)
May 18 Vue.js
springboot+VUE实现登录注册
May 27 Vue.js
vue响应式原理与双向数据的深入解析
Jun 04 Vue.js
vue实现锚点定位功能
Jun 29 Vue.js
Vue Element-ui表单校验规则实现
Jul 09 Vue.js
vue-cli3.0修改打包后的文件名和文件地址,打包后本地运行报错解决
Apr 06 Vue.js
vue自定义右键菜单之全局实现
Apr 09 Vue.js
手动实现vue2.0的双向数据绑定原理详解
Feb 06 #Vue.js
vue3.0 自适应不同分辨率电脑的操作
Feb 06 #Vue.js
vue使用echarts画组织结构图
Feb 06 #Vue.js
vue 根据选择的月份动态展示日期对应的星期几
Feb 06 #Vue.js
解决vue项目本地启动时无法携带cookie的问题
Feb 06 #Vue.js
如何封装Vue Element的table表格组件
Feb 06 #Vue.js
Vue实现圆环进度条的示例
Feb 06 #Vue.js
You might like
一个简单的php加密解密函数(动态加密)
2013/06/19 PHP
解析php如何将日志写进syslog
2013/06/28 PHP
php实现生成验证码实例分享
2016/04/10 PHP
Yii2.0 模态弹出框+ajax提交表单
2016/05/22 PHP
laravel异步监控定时调度器实例详解
2019/06/21 PHP
JavaScript 高级篇之DOM文档,简单封装及调用、动态添加、删除样式(六)
2012/04/07 Javascript
js 为label标签和div标签赋值的方法
2013/08/08 Javascript
一个非常全面的javascript URL解析函数和分段URL解析方法
2014/04/12 Javascript
JavaScript中实现依赖注入的思路分享
2015/01/15 Javascript
45个JavaScript编程注意事项、技巧大全
2015/02/11 Javascript
js实现正则匹配中文标点符号的方法
2015/12/23 Javascript
Jquery对新插入的节点 绑定Click事件失效的解决方法
2016/06/02 Javascript
jquery文字填写自动高度的实现方法
2016/11/07 Javascript
jQuery将表单序列化成一个Object对象的实例
2016/11/29 Javascript
Angular.js中ng-if、ng-show和ng-hide的区别介绍
2017/01/20 Javascript
jQuery Layer弹出层传值到父页面的实现代码
2017/08/17 jQuery
微信小程序简单实现form表单获取输入数据功能示例
2017/11/30 Javascript
详解如何用babel转换es6的class语法
2018/04/03 Javascript
React+Webpack快速上手指南(小结)
2018/08/15 Javascript
js实现简易计算器功能
2019/10/18 Javascript
微信小程序实现左侧滑动导航栏
2020/04/08 Javascript
WebPack工具运行原理及入门教程
2020/12/02 Javascript
JS+CSS实现动态时钟
2021/02/19 Javascript
[27:02]2014 DOTA2国际邀请赛中国区预选赛 5 23 CIS VS LGD第三场
2014/05/24 DOTA
如何利用Fabric自动化你的任务
2016/10/20 Python
python删除过期log文件操作实例解析
2018/01/31 Python
Python中pillow知识点学习
2018/04/30 Python
python单线程下实现多个socket并发过程详解
2019/07/27 Python
纯CSS3实现带动画效果导航菜单无需js
2013/09/27 HTML / CSS
UNIX命令速查表
2012/03/10 面试题
《和田的维吾尔》教学反思
2014/04/14 职场文书
2015年酒店工作总结
2015/04/28 职场文书
2016年4月份红领巾广播稿
2015/12/21 职场文书
《自己的花是让别人看的》教学反思
2016/02/19 职场文书
python3操作redis实现List列表实例
2021/08/04 Python
CSS使用伪类控制边框长度的方法
2022/01/18 HTML / CSS