[Vue] 深入解析Vue中的<keep-alive>组件

前言

<KeepAlive>vue的一个内置组件,它允许我们缓存并保持活动状态的组件 ,以提高性能和用户体验。本文将深入解析Vue中的<keep-alive>组件,探讨其用法、原理以及常见应用场景。

基本使用

默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态------当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。但是我们想要组件能在被"切走"的时候保留它们的状态。这时候我们就可以使用<KeepAlive> 将这些组件包装起来:

vue 复制代码
<template>
  <div>
    <keep-alive>
      <component></component>
    </keep-alive>
  </div>
</template>

这样,component组件就会被缓存起来,而不是每次切换路由或组件时都销毁和重新创建。

<keep-alive>的配置属性

<keep-alive>组件提供了一些属性和配置项,以满足不同场景下的需求。其中,最常用的是includeexclude属性,它们用于指定哪些组件需要被缓存,哪些不需要。

vue 复制代码
<!-- 以英文逗号分隔的字符串 --> 
<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>

此外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存,为新的实例提供空间

vue 复制代码
<KeepAlive :max="10"> 
    <component/> 
</KeepAlive>

生命周期

当组件被包裹在 <keep-alive> 中时,这些组件会触发额外的生命周期钩子。以下是与 <keep-alive> 相关的生命周期钩子函数:

onActivated()

  • 当被包裹的组件切换到活动状态时触发。
  • 当组件首次渲染或被 <keep-alive> 缓存并再次激活时,该钩子函数会被调用。
  • 在这个阶段,你可以执行一些初始化逻辑或者从缓存中恢复状态。

onDeactivated():

  • 当被包裹的组件切换到非活动状态时触发。
  • 当组件即将被缓存时,该钩子函数会被调用。
  • 在这个阶段,你可以执行一些清理逻辑或者保存状态到缓存中。

原理分析

源码

话不多说,先上源码源码位置:src/core/components/keep-alive.js

js 复制代码
export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    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.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render() {
    /* 获取默认插槽中的第一个组件节点 */
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    /* 获取该组件节点的componentOptions */
    const componentOptions = vnode && vnode.componentOptions

    if (componentOptions) {
      /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
      const name = getComponentName(componentOptions)

      const { include, exclude } = this
      /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
      if (
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      /* 获取组件的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
     /*  拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
      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
        /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

可以看到该组件没有template,而是用了render,在组件渲染的时候会自动执行render函数

this.cache是一个对象,用来存储需要缓存的组件,它将以如下形式存储:

js 复制代码
this.cache = {
    'key1':'组件1',
    'key2':'组件2',
    // ...
}

在组件销毁的时候执行pruneCacheEntry函数

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()
  }
  cache[key] = null
  remove(keys, key)
}

mounted钩子函数中观测 includeexclude 的变化,如下:

js 复制代码
mounted () {
    this.$watch('include', val => {
        pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
        pruneCache(this, name => !matches(val, name))
    })
}

如果includeexclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:

js 复制代码
function pruneCache (keepAliveInstance, filter) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode = cache[key]
    if (cachedNode) {
      const name = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象剔除即可

关于keep-alive的最强大缓存功能是在render函数中实现。

首先获取组件的key值:

js 复制代码
const key = vnode.key == null? 
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key

拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:

js 复制代码
/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    /* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */
    remove(keys, key)
    keys.push(key)
} 

直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个

this.cache对象中没有该key值的情况,如下:

js 复制代码
/* 如果没有命中缓存,则将其设置进缓存 */
else {
    cache[key] = vnode
    keys.push(key)
    /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
    if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
}

表明该组件还没有被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys中,此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉。

相关推荐
Cao_Ron13 分钟前
用 SVG image symbol 画 ECharts graph 圆形节点
前端
hxy060117 分钟前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Hilaku18 分钟前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
京东云开发者20 分钟前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron31 分钟前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee40 分钟前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup1 小时前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬1 小时前
短链跳转
前端
孟陬1 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai
Cao_Ron1 小时前
给 ECharts 6 graph 节点叠加角标:从原理到性能优化
前端