写点什么

vue2.x 中 keep-alive 源码解析以及 LRU 缓存策略使用

作者:不叫猫先生
  • 2023-04-30
    北京
  • 本文字数:4741 字

    阅读完需:约 16 分钟

vue2.x中keep-alive源码解析以及LRU缓存策略使用

一、前世尘缘

vue 中内置组件 keep-alive 的设计思想源于 HTTP 中的 Keep-Alive 模式,Keep-Alive 模式避免频繁创建、销毁链接,允许多个请求和响应使用同一个 HTTP 链接。HTTP 1.0 中 keep-alive 默认是关闭的,需要在 HTTP 头加入"Connection: Keep-Alive",才能启用 Keep-Alive;HTTP 1.1 中默认启用 Keep-Alive,如果加入"Connection: close ",才关闭。目前大部分浏览器都是用 HTTP 1.1 协议。

二、keep-alive 内置组件

作用:动态切换组件时缓存组件实例,避免 dom 重新渲染。

1.缓存动态组件

当组件为componentOne时缓存该组件实例


<keep-alive :include="componentOne`" :exclude="componentTwo" :max="num">     <component :is="currentComponent"></component> </keep-alive>
复制代码

2.缓存路由组件

注意缓存路由组件 vue2.x 与 vue3.x 有区别,vue2.x 用法如下:


<keep-alive :include="componentOne`" :exclude="componentTwo" :max="num">     <router-view :is="currentComponent"></router-view> </keep-alive>
复制代码


vue3.x 用法如下:


<router-view v-slot="{ Component }">   <keep-alive :include="includeList">      <component :is="Component"/>   </keep-alive></router-view>
复制代码

3.原理解析

缓存的组件以 [key,vnode] 的形式记录,keys 记录缓存的组件 key,依据 inclued、exclude 的值,并且当超过设置的 max 根据 LUR 算法进行清除。vue2.x 和 vue3.x 相差不大。

(1)keep-alive 在生命周期中做了什么?

  • created:初始化 catch,keys。catch 是一个缓存组件虚拟 dom 的数组,其中数组中对象的 key 是组件的 key,value 是组件的虚拟 dom;keys 是一个用来缓存组件的 key 的数组。

  • mounted:实时监听 include、exclude 属性的变化,并执行相应操作。

  • destroyed:删除掉所有缓存相关的数据。

(2)源码

地址:源码地址


// 源码位置:src/core/components/keep-alive.jsexport default {  name: 'keep-alive',  abstract: true,  props: {    include: patternTypes,    exclude: patternTypes,    max: [String, Number]  },  created () {    this.cache = Object.create(null)    this.keys = []  },  destroyed () {    for (const key in this.cache) {      pruneCacheEntry(this.cache, key, this.keys)    }  },  mounted () {   //查看是否有缓存没有缓存的话直接走缓存    this.cacheVNode()    // 这里借助 watch 监控 include  和 exclude   // 如果有变化的话,则按照最新的 include 和 exclude 更新 this.cache  // 将不满足 include、exclude 限制的 缓存vnode 从 this.cache 中移除    this.$watch('include', val => {      pruneCache(this, name => matches(val, name))    })    this.$watch('exclude', val => {      pruneCache(this, name => !matches(val, name))    })  },  updated() {    this.cacheVNode()  },  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      }    } },  render(){    //下面详细介绍  }}
复制代码

(3)abstract:true

设置为 true 时,表面该组件为抽象组件,抽象组件不会和子组件建立父子关系,组件实例会根据这个属性决定是否忽略该组件,所以并不会有节点渲染在页面中。

(4)pruneCacheEntry 函数

destoryed 周期中循环了所有缓存的组件,并用 pruneCacheEntry进行处理,pruneCacheEntry 做了什么事?


// src/core/components/keep-alive.js
function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode) { const cached = cache[key] if (cached && (!current || cached.tag !== current.tag)) { cached.componentInstance.$destroy() // 执行组件的destory钩子函数 } cache[key] = null // cache中对象的key设为null remove(keys, key) // 删除keys对应的元素}
复制代码


destoryed 周期中,删除缓存组件的所有数组,pruneCacheEntry主要做了这几件事:


  • 遍历缓存组件集合(cach),对所有缓存的组件执行 $destroy 方法

  • 清除 cache 中 key 的值

  • 清除 keys 中的 key

(5)render

render 中主要做了什么?


  • 获取 keep-alive 组件子节点中第一个组件的 vnode、componentOptions、name

  • 如果 name 存在且不在 include 中或者存在在 exclude 中,则返回虚拟 dom。此时该组件并没有使用缓存。

  • 接下来就是上面的 else 情况:使用keep-alive进行组件缓存,根据组件 id,tag 生成组件的 key,如果 cache 集合中存在以 key 为属性名的 vdom,,说明组件已经缓存过,则将缓存的 Vue 实例赋值给 vnode.componentInstance,从 keys 中删除 key,再把 key push 导 keys 中,保证当前 key 在 keys 的最后面(这是 LRU 算法的关键)。如果不存在则继续走下面

  • 如果 cach[key]不存在则为第一次加载组件,则把 vdom 赋值给 cach[key],key push 到 key

  • 如果 keys 的长度大于 max,则进行组件缓存清理,则把不经常使用的被缓存下来的在 keys 中排第一位的组件清除掉,清除也是调用的 pruneCacheEntry 方法


render () {  // 获取 keep-alive 组件子节点中的第一个组件 vnode    const slot = this.$slots.default    const vnode = getFirstComponentChild(slot)    // 获取组件的配置选项对象    const componentOptions = vnode && vnode.componentOptions    if (componentOptions) {      // 获取组件的名称      const name = _getComponentName(componentOptions)      const { include, exclude } = this       // 如果当前的组件 name 不在 include 中或者组件的 name 在 exclude 中      // 说明当前的组件是不被 keep-alive 所缓存的,此时直接 return vnode 即可      if (        // not included        (include && (!name || !matches(include, name))) ||        // excluded        (exclude && name && matches(exclude, name))      ) {        return vnode      }     // 代码执行到这里,说明当前的组件受 keep-alive 组件的缓存      const { cache, keys } = this        // 定义 vnode 缓存用的 key      const key =        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           // 如果 cache[key] 已经存在的话,则说明当前的组件 vnode 已经被缓存过了,此时需要将其恢复还原出来      if (cache[key]) {        // 将缓存的 Vue 实例赋值给 vnode.componentInstance        vnode.componentInstance = cache[key].componentInstance        // make current key freshest          // 先从 keys 中移除 key,然后再 push key,这可以保证当前的 key 在 keys 数组中的最后面        remove(keys, key)        keys.push(key)      } else {        // delay setting the cache until update          // 如果 cache[key] 不存在的话,说明当前的子组件是第一次出现,此时需要将 vnode 缓存到 cache 中,将 key 存储到 keys 字符串数组中。这里是用一个中间变量接收,当数据变化时触发updated去调用cacheVNode方法。        this.vnodeToCache = vnode        this.keyToCache = key      }
// @ts-expect-error can vnode.data can be undefined // 将 vnode.data.keepAlive 属性设置为 true,这对 vnode 有一个标识的作用,标识这个 // vnode 是 keep-alive 组件的 render 函数 return 出去的,这个标识在下面的运行代码中有用 vnode.data.keepAlive = true } return vnode || (slot && slot[0]) }
复制代码

三、LRU 算法


LRU(Least Recently Used)最近最少使用缓存策略,根据历史数据记录,当数据超过了限定空间的时候对数据清理,清理的原则是对很久没有使用到过的数据进行清除

1、为什么要使用 Map 是来定义容器

Map 在保存数据时会按照记住存储数据时候的顺序,这样存储的数据是有序列的,并且会维护键值对的插入顺序,Map 存储数据的键值可以是任意类型(对象或者基本类型都可),Map 提供了 get、set、delete 方法十分方便;而Object的话是无序,当然也可以使用Array。另外 Map 的算法复杂度是 O(1),处理数据更迅速。

2、应用场景

  • redis

  • 浏览器浏览记录

  • vue 中内置组件 keep-alive

3、代码实现

实现的大概思路如下:


  • 创建一个 LRUCache 类

  • 定义容器以及容器的容量

  • 定义 set 方面,设置容器中的数据

  • 定义 get 方法,获取容器中的数据


class LRUCache {  constructor(length) {    // 定义容器容量    this.length = length;    // 创建数据容器,生成一个空映射    this.map = new Map();  }  // 设置key值  set(key, value) {  }  // 获取key值  get(key) {}}
复制代码


接下来就是对 set 方法和 get 方法的处理:


  • set

  • 当容器长度不超过设定的长度:设置 key 值,但是为了达到缓存策略的效果,需要我们先删除数据,后添加到容器的最后一条

  • 当容器长度超过设定的长度:先删除掉容器中的第一条数据

  • get

  • 先获取数据值,然后删除该条数据,再设置数据到最后



class LRUCache { constructor(length) { // 定义容器容量 this.length = length; // 定义数据容器 this.map = new Map(); } // 设置key值 set(key, value) { // 如果容器容量超过设定的容量 if (this.map.size >= this.length) { // 等价于:let firstKey = this.map.keys()[0] //map.keys().next()查询容器中第一条数据的key值 //keys()会返回一个迭代器对象,包含了实力对象中的每一个key值 let firstKey = this.map.keys().next().value; //删除容器中第一条数据 this.map.delete(firstKey); }
// 容器中存在key就先删除掉 if (this.map.has(key)) { this.map.delete(key); } // 删除后重新加入该条数据 this.map.set(key, value); } // 获取key值 get(key) { // 获取key值不存在返回null if (!this.map.has(key)) { return null; } // 获取key值 let value = this.map.get(key); //删除容器中的该条数据 this.map.delete(key); //重新把该条数据添加到容器中 this.map.set(key, value); return value }}// 创建实例对象并设置容器大小const lruCache = new LRUCache(5)
复制代码


添加 6 条数据


        lruCache.set('name', 'zhangsan')    lruCache.set('class', 'xinguan')    lruCache.set('age', 19)    lruCache.set('sex', '男')    lruCache.set('occupation', '前端工程师')    lruCache.set('year', '2023')    console.log(lruCache, 'lruCache')
复制代码


对 lruCache 添加了 6 条数据并按顺序排列,打印出来只剩 5 条数据,添加的第一条('name', 'zhangsan')被删除了。



然后获取 class 的值,发现 key 为 class 的这条数据跑最后了。因为在 get 时候先 delete 后 set 了。


  console.log(lruCache.get('class'), 'lruCache')//xinguan
复制代码



发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2022-10-18 加入

还未添加个人简介

评论

发布
暂无评论
vue2.x中keep-alive源码解析以及LRU缓存策略使用_缓存_不叫猫先生_InfoQ写作社区