从浏览器缓存淘汰策略和vue的keep-alive学习LRU算法
由浏览器的缓存策略引出LRU算法原理
然后走进Vue中keep-alive的应用
接着,透过vue中的keep-alive源码来看LRU算法的实现
最后 来一道leetCodo题目 我们来实现一个LRU算法
一.LRU缓存淘汰策略
缓存在计算机网络上随处可见 例如:当我们首次访问一个网页时 打开很慢,但当我们再次打开时,打开就很快
这就涉及缓存在浏览器上的应用:浏览器缓存。 当我们打开一个网页 它在发起真正的网络请求前,查询浏览器缓存,看是否有要请求的文件 如果有 浏览器将会拦截请求,
返回缓存文件 并直接结束请求,不会再去服务器上下载 如果不存在 才会去服务器请求
其实 浏览器中的缓存是一种在本地保存资源副本 它的大小是有限的,当我们请求数过多的时候 缓存空间会被用满
此时继续进行网络请求就需要确定缓存中哪些数据被保留 哪些数据被移除 这就是浏览器缓存淘汰策略,最常见的策略有FIFO(先进先出),LFU(最少使用)、LRU(最近最少使用)
LRU(Least Recently Used:最近最少使用) 缓存淘汰策略 顾名思义 就是根据数据的历史访问记录来淘汰数据
其核心思想是: 如果数据最近被访问过 那么将来被访问的几率也更高 优先淘汰最近没被访问到的数据
LRU 在keep-alive(Vue) 上的实现
keep-alive 在vue中用于实现组件的缓存 当组件切换时不会对当前组件进行卸载
基本
{/* <keep-alive>
<component :is="view"></component>
</keep-alive> */}
最常用的两个属性:include、exclude 用于组件进行有条件的缓存,可以用逗号分隔字符串、正则表达式或一个数组来表示。
在2.5.0版本中,keep-alive 新增了max属性,用于最多可以缓存多少组件实例,一旦这个数字达到了,在新实例被创建之前,
以缓存组件中最久没有被访问的实例会被销毁掉,看 这里就应用了LRU算法。即在keep-alive 中缓存达到max,
新增缓存实例会优先淘汰最近没有被访问到的实例
下面我们透过vue源码看一下具体的实现
从vue源码看keep-alive的实现
export default {
name:'keep-alive',
//抽象组件属性,它在组件实例建立父子关系的时候会被忽略,发生在initLifecycle的过程中
abstract:true,
props:{
//被缓存组件
include:patternTypes,
// 不被缓存组件
exclude:patternTypes,
// 指定缓存大小
max:[String,Number]
},
created(){
//初始化用于存储缓存的cache对象
this.cache = Object.create(null)
//初始化用于存储VNode key值的keys数组
this.keys = []
},
destroyed() {
for(const key in this.cache){
// 删除所有缓存
pruneCacheEntry(this.cache,key,this.keys);
}
},
mounted(){
// 监听缓存(include)/不缓存(exclude)组件的变化
// 在变化时 重新调整cache
// pruneCacheEntry:遍历cache 如果缓存的节点名称与传入的规则没有匹配上的话 就把这个节点从缓存中移除
this.$watch('include',val=>{
pruneCache(this,name => matches(val,name));
});
this.$watch('exclude',val=>{
pruneCache(this,name=> !matches(val,name))
})
},
render(){
// 获取第一个子元素的 vnode
const slot = this.$slot.default;
const vnode:VNode = getFirstComponentChild(slot)
const componentOptions:?VNodeComponentOptions = vnode && vnode.componentOPtions;
if(componentOptions){
// name不在include中或者在exclude中直接返回vnode,否则继续进行下一步
// 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
// 获取键,优先获取组件的name字段 否则是组件的tag
const key:?string = vnode.key ===null?
//same constructor may get registered as different local components
// so cid alone is not enough
componentOptions.Ctor.cid + (componentOptions.tag?`::${componentOptions.tag}`:""):vnode.key
// 下面就是LRU算法了
// 如果在缓存里有则调整
// 没有 则放入(长度超过max,则淘汰最近没有访问的)
// 如果命中缓存,则从缓存中获取vnode示例 并且调整key的顺序 放入keys数组的末尾
if(cache[key]){
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys,key)
keys.push(key)
}//如果乜有命中缓存 就把vnode 放进缓存
else {
cache[key] = vnode
keys.push(key)
//prune oldest entry
// 如果配置了max 并且缓存的长度超过了this.max 还要从缓存中删除第一个
if(this.max && keys.length>parseInt(this.max)){
pruneCacheEntry(cache,keys[0],keys,this._vnode)
}
}
// keepAlive标记位
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
};
// 移除key缓存
function prunCacheEntry(
cache:VNodeCache,
key:String,
keys:Array<string>,
current?:VNode
){
const cached = cache[key]
if(cached &&(!current ||cached.tag !== current.tag)){
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys,key)
}
// remove 方法(shared/util.js)
// Remove an item from an array
export function remove(arr:Array<any>,item:any):Array<any>|void{
if(arr.length){
const index = arr.indexOf(item)
if(index>-1){
return arr.slice(index,1)
}
}
}
https://github.com/vuejs/vue/blob/dev/src/core/components/keep-alive.js(keep-alive源码路径)
在keep-alive 缓存超过max时,使用的缓存淘汰算法就是LRU算法 它在实现的过程中用到了cache对象用于保存缓存的组件实例及key值,keys数组用于保存缓存组件的key
当keep-alive中渲染一个需要缓存的实例时:
判断缓存中是否已存在了该实例 缓存了则直接获取 并调整key在keys中的位置(移除keys中key,并放入keys数组的最后一位)
如果没有缓存 则缓存该实例 若keys的长度大于max (缓存长度超过上限) 则移除keys[0] 缓存
设计和实现一个LRU(最近最少使用)缓存机制
运用你所掌握的数据结构 设计和实现一个LRU(最近最少使用)缓存机制。
它应该支持以下操作
获取数据get和写入数据put
获取数据get(key)-如果秘钥(key)存在于缓存中,则获取秘钥的值(总是正数),否则返回-1.
写入数据put(key,value)-如果秘钥不存在 则写入数据。当缓存容量达到上限时,它应该再写入新数据之前删除最久未使用的数据,从而为新数据留出空间
进阶
你是否可以在O(1)时间复杂度内完成两种操作?
示例:
/缓存容量/
LRUCache cache = new LRUCache(2)
cache.put(1,1)
cache.put(2,2)
caches.get(1) //返回1
caches.put(3,3)//改操作会使得秘钥2作废
caches.get(2) //返回-1(未找到)
caches.put(4,4) //该操作会使得秘钥1作废
caches.get(1) //返回-1(未找到)
caches.get(3) //返回3
caches.get(4) //返回4
基础解法:数组 + 对象实现
类 vue keep-alive 实现
var LRUCache = function(capacity){
this.keys = []
this.cache = Object.create(null)
this.capacity = capacity
}
LRUCache.prototype.get = function(key){
if(this.cache[key]){
// 调整位置
remove(this.keys,key)
this.keys.push(key)
return this.cache[key]
}
return -1
}
LRUCache.prototype.put = function(key,value){
if(this.cache[key]){
//存在即更新
this.cache[key] = value
remove(this.keys,key)
this.keys.push(key)
}else{
// 不存在即加入
this.keys.push(key)
this.cache[key] = value
// 判断缓存是否已超过最大值
if(this.keys.length>this.capacity){
removeCache(this.cache,this.keys,this.keys[0])
}
}
}
// 移除key
function remove(arr,key){
if(arr.length){
const index = arr.indexOf(key)
if(index>-1){
return arr.splice(index,1)
}
}
}
// 移除缓存中key
function removeCache(cache,keys,key){
cache[key] = null
remove(keys,key)
}
进阶
// 利用Map既能保存键值对 并且能够记住键的原始插入顺序
var LRUCache = function(capacity){
this.cache = new Map()
this.capacity = capacity
}
LRUCache.prototype.get = function(key){
if(this.cache.has(key)){
//存在即刷新
let temp = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key,temp)
return temp
}
return -1
}
LRUCache.prototype.put = function(key,value){
if(this.cache.has(key)){
//存在即更新(删除后加入)
this.cache.delete(key)
}else if(this.cache.size >= this.capacity){
// 不存在即加入
// 缓存超过最大值 则移除最近没有使用的
this.cache.delete(this.cache.keys().next().value)
}
this.cache.set(key,value)
}