如何管理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.js桌面端自定义滚动条组件之美化滚动条VScroll
Dec 01 Vue.js
实用的 vue tags 创建缓存导航的过程实现
Dec 03 Vue.js
vue中如何添加百度统计代码
Dec 19 Vue.js
浅析vue中的nextTick
Dec 28 Vue.js
vue 计算属性和侦听器的使用小结
Jan 25 Vue.js
手写Vue2.0 数据劫持的示例
Mar 04 Vue.js
vue前端工程的搭建
Mar 31 Vue.js
解读Vue组件注册方式
May 15 Vue.js
vue 实现上传组件
May 31 Vue.js
vue使用Google Recaptcha验证的实现示例
Aug 23 Vue.js
vue中控制mock在开发环境使用,在生产环境禁用方式
Apr 06 Vue.js
vue @click.native 绑定原生点击事件
Apr 22 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
简单的cookie计数器实现源码
2013/06/07 PHP
php 使用GD库为页面增加水印示例代码
2014/03/24 PHP
PHP中的Memcache详解
2014/04/05 PHP
PHP获取当前日期和时间及格式化方法参数
2015/05/11 PHP
PHP MySql增删改查的简单实例
2016/06/21 PHP
Windows下php+mysql5.7配置教程
2017/05/16 PHP
jQuery live( type, fn ) 委派事件实现
2009/10/11 Javascript
Javascript selection的兼容性写法介绍
2013/12/20 Javascript
jQuery实现当按下回车键时绑定点击事件
2014/01/28 Javascript
CSS中position属性之fixed实现div居中
2015/12/14 Javascript
JS中mouseover和mouseout多次触发问题如何解决
2016/06/06 Javascript
微信小程序 页面跳转及数据传递详解
2017/03/14 Javascript
详解Node.js项目APM监控之New Relic
2017/05/12 Javascript
Vue v2.4中新增的$attrs及$listeners属性使用教程
2018/01/08 Javascript
vue加载自定义的js文件方法
2018/03/13 Javascript
Node.js + express实现上传大文件的方法分析【图片、文本文件】
2019/03/14 Javascript
Linux下使用python自动修改本机网关代码分享
2015/05/21 Python
python通过elixir包操作mysql数据库实例代码
2018/01/31 Python
利用Python如何生成便签图片详解
2018/07/09 Python
如何通过50行Python代码获取公众号全部文章
2019/07/12 Python
python使用matplotlib绘制雷达图
2019/10/18 Python
Python简易计算器制作方法代码详解
2019/10/31 Python
利用python实现冒泡排序算法实例代码
2019/12/01 Python
python如何获得list或numpy数组中最大元素对应的索引
2020/11/16 Python
非凡女性奢华谦虚风格:The Modist
2017/10/28 全球购物
大整数数相乘的问题
2012/07/22 面试题
如何掌握自荐信格式呢
2013/11/19 职场文书
采购部部门职责
2013/12/15 职场文书
新郎父亲婚宴答谢词
2014/01/11 职场文书
运动会解说词50字
2014/01/18 职场文书
承认错误的检讨书
2014/01/30 职场文书
白岩松演讲
2014/05/21 职场文书
2014年仓管员工作总结
2014/11/18 职场文书
2015年教研工作总结
2015/05/23 职场文书
mysql事务隔离级别详情
2021/10/24 MySQL
Python数据结构之队列详解
2022/03/21 Python