Vue3探秘系列— 组件缓存:keep-alive的实现原理(十五)

前言

Vue3探秘系列文章链接:

不止响应式:Vue3探秘系列--- 虚拟结点vnode的页面挂载之旅(一)

不止响应式:Vue3探秘系列--- 组件更新会发生什么(二)

不止响应式:Vue3探秘系列--- diff算法的完整过程(三)

不止响应式:Vue3探秘系列--- 组件的初始化过程(四)

终于轮到你了:Vue3探秘系列--- 响应式设计(五)

Hello~大家好。我是秋天的一阵风

Vue.js中,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态------当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。

在下面的例子中,你会看到两个有状态的组件------A 有一个计数器,而 B 有一个通过 v-model 同步 input 框输入内容的文字展示。尝试先更改一下任意一个组件的状态,然后切走,再切回来:

你会发现在切回来之后,之前已更改的状态都被重置了。

在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被"切走"的时候保留它们的状态。要解决这个问题,我们可以用 <KeepAlive> 内置组件将这些动态组件包装起来:

javascript 复制代码
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

现在,在组件切换时状态也能被保留了:你可以点击这里查看在线效果

一、keep-alive的几个参数

如果你对 keep-alive的使用已经非常熟悉,可以跳过这一节。

1. 包含与排除

<KeepAlive> 默认会缓存内部的所有组件实例,但我们可以通过 includeexclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:

javascript 复制代码
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>

它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。

2. 最大缓存数

我们可以通过传入 max prop 来限制可被缓存的最大组件实例数。<KeepAlive> 的行为在指定了 max 后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间

javascript 复制代码
<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>

3. 生命周期

当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃 状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活

一个持续存在的组件可以通过 onActivated()onDeactivated() 注册相应的两个状态的生命周期钩子:

javascript 复制代码
<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
})
</script>

请注意:

  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。

二、keep-alive的实现原理

为了更好的理解,我们先看一个示例代码:

javascript 复制代码
<keep-alive>
  <comp-a v-if="flag"></comp-a>
  <comp-b v-else></comp-b>
  <button @click="flag=!flag">toggle</button>
</keep-alive>

可以看到,点击按钮时,flag变量的值会变化,并且comp-a comp-b组件会根据这个变量进行切换

我们可以用模板导出工具看一下它编译后的 render 函数:

javascript 复制代码
import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, KeepAlive as _KeepAlive, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_comp_a = _resolveComponent("comp-a")
  const _component_comp_b = _resolveComponent("comp-b")
  return (_openBlock(), _createBlock(_KeepAlive, null, [
    (_ctx.flag)
      ? _createVNode(_component_comp_a, { key: 0 })
      : _createVNode(_component_comp_b, { key: 1 }),
    _createVNode("button", {
      onClick: $event => (_ctx.flag=!_ctx.flag)
    }, "toggle", 8 /* PROPS */, ["onClick"])
  ], 1024 /* DYNAMIC_SLOTS */))
}

我们使用了 KeepAlive 组件对这两个组件做了一层封装,KeepAlive 是一个抽象组件,它并不会渲染成一个真实的 DOM,只会渲染内部包裹的子节点,并且让内部的子组件在切换的时候,不会走一整套递归卸载和挂载 DOM的流程,从而优化了性能。

1. KeepAlive 组件的定义

javascript 复制代码
const KeepAliveImpl = {
  name: `KeepAlive`,
  __isKeepAlive: true,
  inheritRef: true,
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },
  setup(props, { slots }) {
    const cache = new Map()
    const keys = new Set()
    let current = null
    const instance = getCurrentInstance()
    const parentSuspense = instance.suspense
    const sharedContext = instance.ctx
    const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext
    const storageContainer = createElement('div')
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      const instance = vnode.component
      move(vnode, container, anchor, 0 /* ENTER */, parentSuspense)
      patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, optimized)
      queuePostRenderEffect(() => {
        instance.isDeactivated = false
        if (instance.a) {
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)
    }
    sharedContext.deactivate = (vnode) => {
      const instance = vnode.component
      move(vnode, storageContainer, null, 1 /* LEAVE */, parentSuspense)
      queuePostRenderEffect(() => {
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
        instance.isDeactivated = true
      }, parentSuspense)
    }
    function unmount(vnode) {
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense)
    }
    function pruneCache(filter) {
      cache.forEach((vnode, key) => {
        const name = getName(vnode.type)
        if (name && (!filter || !filter(name))) {
          pruneCacheEntry(key)
        }
      })
    }
    function pruneCacheEntry(key) {
      const cached = cache.get(key)
      if (!current || cached.type !== current.type) {
        unmount(cached)
      }
      else if (current) {
        resetShapeFlag(current)
      }
      cache.delete(key)
      keys.delete(key)
    }
    watch(() => [props.include, props.exclude], ([include, exclude]) => {
      include && pruneCache(name => matches(include, name))
      exclude && !pruneCache(name => matches(exclude, name))
    })
    let pendingCacheKey = null
    const cacheSubtree = () => {
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, instance.subTree)
      }
    }
    onBeforeMount(cacheSubtree)
    onBeforeUpdate(cacheSubtree)
    onBeforeUnmount(() => {
      cache.forEach(cached => {
        const { subTree, suspense } = instance
        if (cached.type === subTree.type) {
          resetShapeFlag(subTree)
          const da = subTree.component.da
          da && queuePostRenderEffect(da, suspense)
          return
        }
        unmount(cached)
      })
    })
    return () => {
      pendingCacheKey = null
      if (!slots.default) {
        return null
      }
      const children = slots.default()
      let vnode = children[0]
      if (children.length > 1) {
        if ((process.env.NODE_ENV !== 'production')) {
          warn(`KeepAlive should contain exactly one component child.`)
        }
        current = null
        return children
      }
      else if (!isVNode(vnode) ||
        !(vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */)) {
        current = null
        return vnode
      }
      const comp = vnode.type
      const name = getName(comp)
      const { include, exclude, max } = props
      if ((include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))) {
        return (current = vnode)
      }
      const key = vnode.key == null ? comp : vnode.key
      const cachedVNode = cache.get(key)
      if (vnode.el) {
        vnode = cloneVNode(vnode)
      }
      pendingCacheKey = key
      if (cachedVNode) {
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
        vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */
        keys.delete(key)
        keys.add(key)
      }
      else {
        keys.add(key)
        if (max && keys.size > parseInt(max, 10)) {
          pruneCacheEntry(keys.values().next().value)
        }
      }
      vnode.shapeFlag |= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */
      current = vnode
      return vnode
    }
  }
}

我们将keep-alive拆分为四个部分,分别是组件渲染缓存props卸载

(1)组件的渲染

我们之前在探究setup的章节时知道,setup函数的返回值可以是对象 ,也可以是函数

如果是函数那么这个函数就是组件的render函数:

javascript 复制代码
return () => {
  pendingCacheKey = null
  if (!slots.default) {
    return null
  }
  const children = slots.default()
  let vnode = children[0]
  if (children.length > 1) {
    if ((process.env.NODE_ENV !== 'production')) {
      warn(`KeepAlive should contain exactly one component child.`)
    }
    current = null
    return children
  }
  else if (!isVNode(vnode) ||
    !(vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */)) {
    current = null
    return vnode
  }
  const comp = vnode.type
  const name = getName(comp)
  const { include, exclude, max } = props
  if ((include && (!name || !matches(include, name))) ||
    (exclude && name && matches(exclude, name))) {
    return (current = vnode)
  }
  const key = vnode.key == null ? comp : vnode.key
  const cachedVNode = cache.get(key)
  if (vnode.el) {
    vnode = cloneVNode(vnode)
  }
  pendingCacheKey = key
  if (cachedVNode) {
    vnode.el = cachedVNode.el
    vnode.component = cachedVNode.component
    // 避免 vnode 节点作为新节点被挂载
    vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */
    // 让这个 key 始终新鲜
    keys.delete(key)
    keys.add(key)
  }
  else {
    keys.add(key)
    // 删除最久不用的 key,符合 LRU 思想
    if (max && keys.size > parseInt(max, 10)) {
      pruneCacheEntry(keys.values().next().value)
    }
  }
  // 避免 vnode 被卸载
  vnode.shapeFlag |= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */
  current = vnode
  return vnode
}
  1. 首先会从slots.default拿到包裹的子节点children,判断如果长度大于1就会报出警告只能包含一个确切的组件

  2. 如果不考虑缓存的情况下,return出去的vnode其实就是children数组第一个子节点

  3. 我们在开头提到过一句:keep-alive是一个抽象组件 ,原因就是keep-alive只负责渲染子节点,它自己本身没有具体的实体内容。

(2)缓存的设计

我们先思考一下,如果让你来实现缓存,该如何实现呢?

换个思路来说,我们是不是可以把缓存理解成:不需要重复渲染。

如果在渲染Dom的时候,我们做一些Dom的缓存处理,在下一次渲染的时候把之前缓存的Dom取出来渲染是不是就能实现了呢?

我们来关注下 KeepAlive 组件的生命周期,它注入了两个钩子函数,onBeforeMountonBeforeUpdate,在这两个钩子函数内部都执行了 cacheSubtree 函数来做缓存:

javascript 复制代码
const cacheSubtree = () => {
  if (pendingCacheKey != null) {
    cache.set(pendingCacheKey, instance.subTree)
  }
}

在第一次渲染之前时会执行onBeforeMountpendingCacheKey还没来得及被赋值,所以第一次渲染的组件并不会被缓存。

第一次执行完setuprender函数以后,这个时候pendingCacheKey才会被赋值。

这个pendingCacheKey的值是vnode身上的属性key,你可能会疑惑,这个key啥时候添加上的?

我们可以看看它的渲染模板:

javascript 复制代码
import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, KeepAlive as _KeepAlive, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_comp_a = _resolveComponent("comp-a")
  const _component_comp_b = _resolveComponent("comp-b")
  return (_openBlock(), _createBlock(_KeepAlive, null, [
    (_ctx.flag)
      ? _createVNode(_component_comp_a, { key: 0 })
      : _createVNode(_component_comp_b, { key: 1 }),
    _createVNode("button", {
      onClick: $event => (_ctx.flag=!_ctx.flag)
    }, "toggle", 8 /* PROPS */, ["onClick"])
  ], 1024 /* DYNAMIC_SLOTS */))
}

我们注意到KeepAlive的子节点创建的时候都添加了一个 key prop,它就是专门为 KeepAlive 的缓存设计的,这样每一个子节点都能有一个唯一的 key

缓存流程
  1. 页面首先渲染 A组件 ,接着当我们点击按钮的时候,修改了flag的值,会触发当前组件的重新渲染,进而也触发了 KeepAlvie 组件的重新渲染,在组件重新渲染前,会执行 onBeforeUpdate 对应的钩子函数,也就再次执行到 cacheSubtree 函数中。

这个时候 pendingCacheKey 对应的是 A 组件 vnode keyinstance.subTree 对应的也是 A 组件的渲染子树,所以 KeepAlive 每次在更新前,会缓存前一个组件的渲染子树

  1. 这个时候渲染了 B组件,当我们再次点击按钮,修改 flag 值的时候,会再次触发KeepAlvie 组件的重新渲染,当然此时执行 onBeforeUpdate 钩子函数缓存的就是 B组件的渲染子树了。

  2. 接着再次执行KeepAlive组件的 render 函数,此时就可以从缓存中根据 A 组件的key拿到对应的渲染子树cachedVNode的了,然后执行如下逻辑:

javascript 复制代码
if (cachedVNode) {
  vnode.el = cachedVNode.el
  vnode.component = cachedVNode.component
  // 避免 vnode 节点作为新节点被挂载
  vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */
  // 让这个 key 始终新鲜
  keys.delete(key)
  keys.add(key)
}
else {
  keys.add(key)
  // 删除最久不用的 key,符合 LRU 思想
  if (max && keys.size > parseInt(max, 10)) {
    pruneCacheEntry(keys.values().next().value)
  }
}

有了缓存的渲染子树后,我们就可以直接拿到它对应的 DOM 以及组件实例 component,赋值给KeepAlivevnode并更新 vnode.shapeFlag,以便后续 patch 阶段使用。

patch函数

虽然拿到了缓存的组件实例并且已经赋值,但是这还没完全实现缓存功能,我们还是得请出我们的老演员 patch 函数,看看组件渲染的时候有keep-alive缓存和没有缓存有什么区别。

patch函数会进入到 processComponent分支,所以我们直接从processComponent开始看

javascript 复制代码
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  if (n1 == null) {
    // 处理 KeepAlive 组件
    if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
      parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized)
    }
    else {
      // 挂载组件
      mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
    }
  }
  else {
    // 更新组件
  }
}

KeepAlive 首次渲染某一个子节点时,和正常的组件节点渲染没有区别,但是有缓存后,由于标记了 shapeFlag,所以在执行processComponent函数时会走到处理 KeepAlive 组件的逻辑中,执行 KeepAlive 组件实例上下文中的 activate函数,我们来看它的实现:

activate 函数
javascript 复制代码
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  const instance = vnode.component
  move(vnode, container, anchor, 0 /* ENTER */, parentSuspense)
  patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, optimized)
  queuePostRenderEffect(() => {
    instance.isDeactivated = false
    if (instance.a) {
      invokeArrayFns(instance.a)
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeMounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
  }, parentSuspense)
}

可以看到,由于此时已经能从 vnode.el 中拿到缓存的 DOM 了,所以可以直接调用move方法挂载节点,然后执行 patch 方法更新组件,以防止 props 发生变化的情况。

接下来,就是通过 queuePostRenderEffect 的方式,在组件渲染完毕后,执行子节点组件定义的 activated 钩子函数。

至此,我们就了解了 KeepAlive 的缓存设计,KeepAlive 包裹的子组件在其渲染后,下一次 KeepAlive 组件更新前会被缓存,缓存后的子组件在下一次渲染的时候直接从缓存中拿到子树vnode以及对应的DOM元素,直接渲染即可。

(3)Props 设计

KeepAlive 一共支持了三个 Props,分别是 include、exclude 和 max

javascript 复制代码
props: {
  include: [String, RegExp, Array],
  exclude: [String, RegExp, Array],
  max: [String, Number]
}
include、exclude : 包含与排除
javascript 复制代码
const { include, exclude, max } = props
if ((include && (!name || !matches(include, name))) ||
  (exclude && name && matches(exclude, name))) {
  return (current = vnode)
}

很好理解,如果子组件名称不匹配 includevnode ,以及子组件名称匹配 exclude vnode 都不应该被缓存,而应该直接返回。

当然,由于props是响应式的,在 includeexclude props 发生变化的时候也应该有相关的处理逻辑,如下:

javascript 复制代码
watch(() => [props.include, props.exclude], ([include, exclude]) => {
  include && pruneCache(name => matches(include, name))
  exclude && !pruneCache(name => matches(exclude, name))
})

监听的逻辑也很简单,当 include 发生变化的时候,从缓存中删除那些name不匹配 include vnode 节点

exclude 发生变化的时候,从缓存中删除那些name匹配 excludevnode 节点。

max: 最大缓存数

KeepAlive还提供一个max参数让我们可以限制缓存的个数,因为缓存本身就是占用内存的,无限制的缓存可能会导致性能问题。

javascript 复制代码
keys.add(key)
// 删除最久不用的 key,符合 LRU 思想
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value)
}

由于新的缓存 key 都是在 keys 的结尾添加的,所以当缓存的个数超过 max 的时候,就从最前面开始删除,符合 LRU 最近最少使用的算法思想。关于LRU,我们会在下一个章节会具体介绍。

(4)组件的卸载

前面我们提到 KeepAlive 渲染的过程实际上是渲染它的第一个子组件节点,并且会给渲染的 vnode 打上如下标记: vnode.shapeFlag |= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */

加上这个 shapeFlag 有什么用呢,我们结合前面的示例来分析。

javascript 复制代码
<keep-alive>
  <comp-a v-if="flag"></comp-a>
  <comp-b v-else></comp-b>
  <button @click="flag=!flag">toggle</button>
</keep-alive>

flagtrue 的时候,渲染 A 组件,然后我们点击按钮修改 flag 的值,会触发KeepAlive组件的重新渲染,会先执行 BeforeUpdate 钩子函数缓存 A 组件对应的渲染子树 vnode,然后再执行 patch 更新子组件。

这个时候会执行 B 组件的渲染,以及 A 组件的卸载,我们知道组件的卸载会执行 unmount 方法,其中有一个关于 KeepAlive 组件的逻辑,如下:

deactivate
javascript 复制代码
const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) => {
  const { shapeFlag  } = vnode
  if (shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
    parentComponent.ctx.deactivate(vnode)
    return
  }
  // 卸载组件
}

如果 shapeFlag 满足 KeepAlive 的条件,则执行相应的deactivate函数,它的定义如下

javascript 复制代码
sharedContext.deactivate = (vnode) => {
  const instance = vnode.component
  move(vnode, storageContainer, null, 1 /* LEAVE */, parentSuspense)
  queuePostRenderEffect(() => {
    if (instance.da) {
      invokeArrayFns(instance.da)
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
    instance.isDeactivated = true
  }, parentSuspense)
}

函数首先通过 move 方法从 DOM 树中移除该节点,接着通过 queuePostRenderEffect 的方式执行定义的 deactivated 钩子函数。

注意,这里我们只是移除了 DOM,并没有真正意义上的执行子组件的整套卸载流程。

那么除了点击按钮引起子组件的卸载之外,当 KeepAlive 所在的组件卸载时,由于卸载的递归特性,也会触发 KeepAlive 组件的卸载,在卸载的过程中会执行 onBeforeUnmount 钩子函数,如下:

javascript 复制代码
onBeforeUnmount(() => {
  cache.forEach(cached => {
    const { subTree, suspense } = instance
    if (cached.type === subTree.type) {
      resetShapeFlag(subTree)
      const da = subTree.component.da
      da && queuePostRenderEffect(da, suspense)
      return
    }
    unmount(cached)
  })
})  

它会遍历所有缓存的 vnode,并且比对缓存的 vnode 是不是当前 KeepAlive 组件渲染的 vnode

如果是的话,则执行 resetShapeFlag 方法,它的作用是修改vnodeshapeFlag,不让它再被当作一个 KeepAlivevnode 了,这样就可以走正常的卸载逻辑。

接着通过 queuePostRenderEffect 的方式执行子组件的 deactivated 钩子函数。

如果不是,则执行 unmount 方法重置 shapeFlag 以及执行缓存 vnode 的整套卸载流程。

三、LRU算法

LRU(Least Recently Used)算法是一种常用的缓存淘汰策略,用于在缓存空间有限的情况下,根据数据项的访问时间顺序来决定哪些数据项应该被优先淘汰。在 Vue.js 中,keep-alive 组件就是利用 LRU 算法来管理缓存的组件实例。

1. LRU 缓存的基本原理

LRU 算法的核心在于维护一个有序的数据结构,用来记录数据项的访问顺序。当缓存达到最大容量时,需要淘汰最久未被访问的数据项。常见的数据结构选择包括双向链表结合哈希表

2. 使用 JavaScript 实现 LRU 缓存

下面是一个使用 JavaScript 实现的 LRU 缓存示例:

javascript 复制代码
class LRUCache {
  constructor(capacity) {
    // 初始化缓存容量
    this.capacity = capacity;
    // 使用 Map 存储键值对
    this.map = new Map();
    // 使用数组记录访问顺序,数组尾部是最近使用的项
    this.keys = [];
  }

  get(key) {
    // 查找键是否存在于缓存中
    const index = this.keys.indexOf(key);
    if (index === -1) return -1; // 如果不存在,返回 -1

    // 将访问过的键移动到数组的末尾,表示它是最近使用的
    this.keys.splice(index, 1);
    this.keys.push(key);

    // 返回键对应的值
    return this.map.get(key);
  }

  put(key, value) {
    // 如果键已经存在,则更新其值,并将其移动到数组末尾
    if (this.map.has(key)) {
      this.map.set(key, value);
      const index = this.keys.indexOf(key);
      this.keys.splice(index, 1);
      this.keys.push(key);
    } else {
      // 如果缓存已满,则移除最久未使用的项
      if (this.keys.length >= this.capacity) {
        const lruKey = this.keys.shift(); // 移除数组头部的键
        this.map.delete(lruKey); // 从 Map 中删除该键
      }

      // 添加新的键值对
      this.map.set(key, value);
      this.keys.push(key); // 将新键添加到数组末尾
    }
  }
}

// 示例使用
const cache = new LRUCache(2);

cache.put(1, 1);
cache.put(2, 2);
console.log(cache.get(1)); // 返回 1
cache.put(3, 3);           // 移除键 2
console.log(cache.get(2)); // 返回 -1 (未找到)
cache.put(4, 4);           // 移除键 1
console.log(cache.get(1)); // 返回 -1 (未找到)
console.log(cache.get(3)); // 返回 3
console.log(cache.get(4)); // 返回 4

总结

好了,到这里本篇的探究就结束了。在开头,我们先了解了KeepAlive组件的基本使用方法,知道了KeepAlive组件是一个抽象组件,只会渲染插槽的子vnode,它自身并没有任何实际内容。我们从渲染缓存props卸载 四个维度去探究了KeepAlive组件的源码实现,在渲染,卸载时由于shapeFlag的不同processComponent方法和unmount方法都会去执行KeepAlive专门的处理分支。最后,我们还简单了解了LRU算法的实现。

相关推荐
customer0810 分钟前
【开源免费】基于SpringBoot+Vue.JS个人博客系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
乐多_L26 分钟前
使用vue3框架vue-next-admin导出表格excel(带图片)
前端·javascript·vue.js
初尘屿风37 分钟前
基于微信小程序的电影院订票选座系统的设计与实现,SSM+Vue+毕业论文+开题报告+任务书+指导搭建视频
vue.js·微信小程序·小程序
南望无一40 分钟前
React Native 0.70.x如何从本地安卓源码(ReactAndroid)构建
前端·react native
Mike_188702783511 小时前
1688代采下单API接口使用指南:实现商品采集与自动化下单
前端·python·自动化
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS打卡健康评测系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
鲨鱼辣椒️面1 小时前
HTML视口动画
前端·html
一小路一1 小时前
Go Web 开发基础:从入门到实战
服务器·前端·后端·面试·golang
堇舟1 小时前
HTML第一节
前端·html
纯粹要努力1 小时前
前端跨域问题及解决方案
前端·javascript·面试