Vue中强大的状态保持组件:keep-alive详解‌

目录

  1. 简介:为什么需要 keep-alive
  2. keep-alive 基本概念与用法

    1. 什么是 keep-alive
    2. 基本用法示例
  3. 动态组件缓存

    1. 动态组件场景
    2. 结合 componentkeep-alive
    3. 图解:动态组件与缓存流程
  4. include/exclude 属性详解

    1. include:白名单模式
    2. exclude:黑名单模式
    3. 示例代码:有条件地缓存组件
  5. 缓存大小限制:max 属性

    1. LRU(最近最少使用)淘汰策略
    2. 示例代码:限制缓存数目
  6. 生命周期钩子:activateddeactivated

    1. 钩子触发时机
    2. 示例:监测组件缓存与激活
  7. 实际场景演示:Tab 页面状态保持

    1. 场景描述
    2. 完整示例代码
    3. ASCII 图解:Tab 页面缓存流程
  8. 常见误区与注意事项
  9. 总结与最佳实践

1. 简介:为什么需要 keep-alive

在传统的单页面项目中,切换路由或动态切换组件往往会销毁上一个组件的实例,导致其中的数据、滚动位置、输入状态等全部丢失。如果用户切换回来,组件会重新创建,所有状态需要重新初始化、重新请求数据,给人一种“界面闪烁”、“体验割裂”的感觉。

keep-alive 是 Vue 内置的一个抽象组件,它可以对被包裹的组件做内存缓存,而不是简单地销毁。当组件状态被“缓存”后,下次切换回来时会快速恢复到上次状态,不必重新执行 createdmounted 等钩子,从而实现“状态保持”的目的。常见应用场景包括:

  • 多标签页切换时保持表单输入、滚动位置等状态
  • 路由切换时保留页面数据,减少不必要的请求
  • 数据量较大,需要频繁返回时避免重新渲染

2. keep-alive 基本概念与用法

2.1 什么是 keep-alive

keep-alive 并不是一个真实渲染到 DOM 的组件,它是一个抽象组件。当你在 Vue 模板中将某个组件包裹在 <keep-alive> 中时,Vue 不会真正销毁该子组件,而是将其保存在内存中。当再次激活时,keep-alive 会恢复该组件的状态。

<keep-alive>
  <my-component v-if="isShown"></my-component>
</keep-alive>
  • isShowntrue 变成 falsemy-component 会被移出 DOM,但并未真正销毁,而是被缓存在内存中。
  • isShown 重新变为 truemy-component 只会触发 activated 钩子,而不会重新执行 createdmounted 等生命周期方法。

2.2 基本用法示例

假设有如下组件,在打开/关闭时打印日志:

<!-- MyComponent.vue -->
<template>
  <div>
    <h3>MyComponent 内容</h3>
    <p>计数:{{ count }}</p>
    <button @click="count++">增加</button>
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      count: 0
    };
  },
  created() {
    console.log('MyComponent created');
  },
  mounted() {
    console.log('MyComponent mounted');
  },
  destroyed() {
    console.log('MyComponent destroyed');
  },
  activated() {
    console.log('MyComponent activated');
  },
  deactivated() {
    console.log('MyComponent deactivated');
  }
};
</script>

在父组件中包裹 keep-alive

<!-- App.vue -->
<template>
  <div>
    <button @click="toggle">切换组件</button>
    <keep-alive>
      <MyComponent v-if="visible" />
    </keep-alive>
  </div>
</template>

<script>
import MyComponent from './MyComponent.vue';

export default {
  components: { MyComponent },
  data() {
    return {
      visible: true
    };
  },
  methods: {
    toggle() {
      this.visible = !this.visible;
    }
  }
};
</script>

操作流程:

  1. 初始化时 visible = trueMyComponent 执行 createdmounted,并显示计数为 0。
  2. 点击 “切换组件” 将 visible 设为 false,此时 MyComponent 会触发 deactivated(并未触发 destroyed),并从 DOM 中移除。
  3. 再次点击将 visible 设为 trueMyComponent 会触发 activated,重新插入到 DOM 中,但内部状态(count)保持原来值

控制台日志示例:

MyComponent created
MyComponent mounted
MyComponent deactivated   <-- 移出 DOM,但未销毁
MyComponent activated     <-- 重新渲染时触发

3. 动态组件缓存

3.1 动态组件场景

在实际业务中,常常会根据不同参数或用户操作渲染不同的组件。例如使用 <component :is="currentView"> 动态切换视图,或者通过路由 router-view 渲染不同页面。此时如果不使用缓存,每次切换会重新创建新组件实例;若组件数据量较大或者需要保持滚动位置,就需要缓存它们的状态。

3.2 结合 componentkeep-alive

<template>
  <div>
    <button @click="currentView = 'ViewA'">切换到 ViewA</button>
    <button @click="currentView = 'ViewB'">切换到 ViewB</button>
    <keep-alive>
      <component :is="currentView" />
    </keep-alive>
  </div>
</template>

<script>
import ViewA from './ViewA.vue';
import ViewB from './ViewB.vue';

export default {
  components: { ViewA, ViewB },
  data() {
    return {
      currentView: 'ViewA'
    };
  }
};
</script>
  • currentView'ViewA' 切换到 'ViewB' 时,ViewA 会触发 deactivated,但并未销毁。
  • 再切换回 'ViewA' 时,ViewA 会触发 activated,内部状态保持。

3.3 图解:动态组件与缓存流程

┌───────────────────────────────────────────┐
│                 初始渲染                  │
│ currentView = 'ViewA'                    │
│ ┌───────────────────────────────────────┐ │
│ │ keep-alive 渲染 ViewA                │ │
│ │   MyViewA created & mounted          │ │
│ └───────────────────────────────────────┘ │
└───────────────────────────────────────────┘
                ↓ 切换到 ViewB               
┌───────────────────────────────────────────┐
│ currentView = 'ViewB'                    │
│ ┌───────────────────────────────────────┐ │
│ │ keep-alive deactivated ViewA         │ │
│ │ (ViewA 未销毁,只是隐藏且缓存状态)     │ │
│ └───────────────────────────────────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ keep-alive 渲染 ViewB                │ │
│ │   MyViewB created & mounted          │ │
│ └───────────────────────────────────────┘ │
└───────────────────────────────────────────┘
                ↓ 切换回 ViewA               
┌───────────────────────────────────────────┐
│ currentView = 'ViewA'                    │
│ ┌───────────────────────────────────────┐ │
│ │ keep-alive 激活 ViewA                │ │
│ │   MyViewA activated                  │ │
│ │   (恢复之前的状态,无需 re-create)    │ │
│ └───────────────────────────────────────┘ │
└───────────────────────────────────────────┘

4. include/exclude 属性详解

有时只希望对部分组件做缓存,或排除某些组件,这时可以通过 include/exclude 进行精确控制。

<keep-alive include="ViewA,ViewB" exclude="ViewC">
  <component :is="currentView" />
</keep-alive>
  • include(白名单):只缓存名称在列表中的组件
  • exclude(黑名单):不缓存名称在列表中的组件(也可使用正则表达式或数组)

4.1 include:白名单模式

<keep-alive include="ViewA,ViewB">
  <component :is="currentView" />
</keep-alive>
  • 只有 ViewAViewB 会被缓存,切换到这两个组件之间会保持状态。
  • 切换到其他组件(如 ViewC)时,不会缓存,离开时会触发 destroyed

示例:

<template>
  <div>
    <button @click="currentView = 'ViewA'">A</button>
    <button @click="currentView = 'ViewB'">B</button>
    <button @click="currentView = 'ViewC'">C</button>
    <keep-alive include="ViewA,ViewB">
      <component :is="currentView" />
    </keep-alive>
  </div>
</template>
  • 切换 A↔B:状态保持
  • 切换到 C:A 或 B 分别会触发 deactivated,但 C 会重新创建,离开 C 时会触发 destroyed

4.2 exclude:黑名单模式

<keep-alive exclude="ViewC">
  <component :is="currentView" />
</keep-alive>
  • 只要组件名称是 ViewC,就不会被缓存;其他组件都缓存。

示例:

<template>
  <div>
    <button @click="currentView = 'ViewA'">A</button>
    <button @click="currentView = 'ViewB'">B</button>
    <button @click="currentView = 'ViewC'">C</button>
    <keep-alive exclude="ViewC">
      <component :is="currentView" />
    </keep-alive>
  </div>
</template>
  • 切换到 ViewC:每次都会重新创建/销毁,不走缓存
  • 切换到 ViewAViewB:由缓存管理,状态保持

4.3 示例代码:有条件地缓存组件

<template>
  <div>
    <label>选择组件进行渲染:</label>
    <select v-model="currentView">
      <option>ViewA</option>
      <option>ViewB</option>
      <option>ViewC</option>
    </select>
    <br /><br />
    <keep-alive :include="['ViewA', 'ViewB']">
      <component :is="currentView" />
    </keep-alive>
  </div>
</template>

<script>
import ViewA from './ViewA.vue';
import ViewB from './ViewB.vue';
import ViewC from './ViewC.vue';

export default {
  components: { ViewA, ViewB, ViewC },
  data() {
    return {
      currentView: 'ViewA'
    };
  }
};
</script>
  • currentViewViewAViewB 时,会缓存组件;为 ViewC 时则不缓存。

5. 缓存大小限制:max 属性

在实际项目中,如果缓存了过多组件,可能导致内存占用过大。keep-alive 提供 max 属性,用于限制最多缓存的组件实例数,超过时会按照 LRU(最近最少使用)策略淘汰最久未使用的实例。

<keep-alive max="3">
  <component :is="currentView" />
</keep-alive>
  • max="3" 表示最多缓存 3 个组件实例,一旦超过,会剔除最早被遗忘的那个。

5.1 LRU(最近最少使用)淘汰策略

假设按顺序切换组件:A → B → C → D → E,当 max=3 时,缓存最多保存 3 个:

  1. 进入 A:缓存 [A]
  2. 切换 B:缓存 [A, B]
  3. 切换 C:缓存 [A, B, C]
  4. 切换 D:缓存达到 3,需淘汰最早未使用的 A,缓存变为 [B, C, D]
  5. 切换 E:淘汰 B,缓存 [C, D, E]
时间轴:A → B → C → D → E
缓存变化:
[A] → [A,B] → [A,B,C] → [B,C,D] → [C,D,E]

5.2 示例代码:限制缓存数目

<template>
  <div>
    <button v-for="v in views" :key="v" @click="currentView = v">
      {{ v }}
    </button>
    <br /><br />
    <!-- 只缓存最近 2 个组件 -->
    <keep-alive :max="2">
      <component :is="currentView" />
    </keep-alive>
  </div>
</template>

<script>
import ViewA from './ViewA.vue';
import ViewB from './ViewB.vue';
import ViewC from './ViewC.vue';
import ViewD from './ViewD.vue';

export default {
  components: { ViewA, ViewB, ViewC, ViewD },
  data() {
    return {
      views: ['ViewA', 'ViewB', 'ViewC', 'ViewD'],
      currentView: 'ViewA'
    };
  }
};
</script>
  • 初始缓存 ViewA
  • 切换 ViewB,缓存 [A, B]
  • 切换 ViewC,淘汰 A,缓存 [B, C]
  • 依次类推

6. 生命周期钩子:activateddeactivated

除了常规生命周期(createdmounteddestroyed),keep-alive 还提供了两个特殊钩子,用于监听组件被缓存/激活状态的变化:

  • activated:当组件从缓存中恢复、重新插入到 DOM 时调用
  • deactivated:当组件被移出 DOM 并缓存时调用

6.1 钩子触发时机

┌───────────────┐
│ 初次渲染      │
│ created       │
│ mounted       │
└───────┬───────┘
        │ 切换 away
        ▼
┌──────────────────┐
│ deactivated      │  (组件移出,但未 destroyed)
└────────┬─────────┘
         │ 切换回
         ▼
┌──────────────────┐
│ activated        │  (组件重新插入,无需重新 created/mounted)
└──────────────────┘
         │ 最终卸载(非 keep-alive 场景)
         ▼
┌──────────────────┐
│ destroyed        │
└──────────────────┘

6.2 示例:监测组件缓存与激活

<!-- CacheDemo.vue -->
<template>
  <div>
    <h3>CacheDemo: {{ message }}</h3>
    <button @click="message = '已修改时间:' + Date.now()">修改 message</button>
  </div>
</template>

<script>
export default {
  name: 'CacheDemo',
  data() {
    return {
      message: '初始内容'
    };
  },
  created() {
    console.log('CacheDemo created');
  },
  mounted() {
    console.log('CacheDemo mounted');
  },
  activated() {
    console.log('CacheDemo activated');
  },
  deactivated() {
    console.log('CacheDemo deactivated');
  },
  destroyed() {
    console.log('CacheDemo destroyed');
  }
};
</script>

配合父组件:

<template>
  <div>
    <button @click="visible = !visible">切换 CacheDemo</button>
    <keep-alive>
      <CacheDemo v-if="visible" />
    </keep-alive>
  </div>
</template>

<script>
import CacheDemo from './CacheDemo.vue';
export default {
  components: { CacheDemo },
  data() {
    return { visible: true };
  }
};
</script>

控制台日志示例:

CacheDemo created
CacheDemo mounted

// 点击 切换 CacheDemo (设 visible=false)
CacheDemo deactivated

// 再次 点击 切换 CacheDemo (设 visible=true)
CacheDemo activated

// 若不使用 keep-alive,直接销毁后切换回来:
CacheDemo destroyed
CacheDemo created
CacheDemo mounted

7. 实际场景演示:Tab 页面状态保持

7.1 场景描述

假设有一个多标签页(Tabs)界面,用户切换不同选项卡时,希望各选项卡内部表单输入、滚动条位置、数据状态都能保持,不会重置。

7.2 完整示例代码

<!-- src/components/TabWithKeepAlive.vue -->
<template>
  <div class="tabs-container">
    <el-tabs v-model="activeName" @tab-click="handleTabClick">
      <el-tab-pane label="表单A" name="formA"></el-tab-pane>
      <el-tab-pane label="列表B" name="listB"></el-tab-pane>
      <el-tab-pane label="表单C" name="formC"></el-tab-pane>
    </el-tabs>

    <keep-alive>
      <component :is="currentTabComponent" />
    </keep-alive>
  </div>
</template>

<script>
// 假设 FormA.vue、ListB.vue、FormC.vue 已创建
import FormA from './FormA.vue';
import ListB from './ListB.vue';
import FormC from './FormC.vue';

export default {
  name: 'TabWithKeepAlive',
  components: { FormA, ListB, FormC },
  data() {
    return {
      activeName: 'formA'
    };
  },
  computed: {
    currentTabComponent() {
      switch (this.activeName) {
        case 'formA':
          return 'FormA';
        case 'listB':
          return 'ListB';
        case 'formC':
          return 'FormC';
      }
    }
  },
  methods: {
    handleTabClick(tab) {
      // 切换时无需做额外操作,keep-alive 会保持状态
      console.log('切换到标签:', tab.name);
    }
  }
};
</script>

<style scoped>
.tabs-container {
  margin: 20px;
}
</style>

示例中的三个子组件:

  • FormA.vue:包含一个输入框和一个文本区,用于演示表单状态保持
  • ListB.vue:包含一个长列表,滚动到某个位置后切换回来,保持滚动
  • FormC.vue:另一个表单示例

其中以 ListB.vue 为例,演示滚动位置保持:

<!-- ListB.vue -->
<template>
  <div class="list-container" ref="scrollContainer" @scroll="onScroll">
    <div v-for="i in 100" :key="i" class="list-item">
      列表项 {{ i }}
    </div>
  </div>
</template>

<script>
export default {
  name: 'ListB',
  data() {
    return {
      scrollTop: 0
    };
  },
  mounted() {
    // 恢复上次 scrollTop
    this.$refs.scrollContainer.scrollTop = this.scrollTop;
  },
  beforeDestroy() {
    // 保存 scrollTop
    this.scrollTop = this.$refs.scrollContainer.scrollTop;
  },
  methods: {
    onScroll() {
      this.scrollTop = this.$refs.scrollContainer.scrollTop;
    }
  }
};
</script>

<style scoped>
.list-container {
  height: 200px;
  overflow-y: auto;
  border: 1px solid #ccc;
}
.list-item {
  height: 30px;
  line-height: 30px;
  padding: 0 10px;
  border-bottom: 1px dashed #eee;
}
</style>
注意:ListB.vue 中使用了 beforeDestroy,但若被 keep-alive 缓存时,beforeDestroy 不会触发。应该使用 deactivated 钩子来保存滚动位置,使用 activated 恢复:
export default {
  name: 'ListB',
  data() {
    return {
      scrollTop: 0
    };
  },
  activated() {
    this.$refs.scrollContainer.scrollTop = this.scrollTop;
  },
  deactivated() {
    this.scrollTop = this.$refs.scrollContainer.scrollTop;
  },
  methods: {
    onScroll() {
      this.scrollTop = this.$refs.scrollContainer.scrollTop;
    }
  }
};

7.3 ASCII 图解:Tab 页面缓存流程

┌───────────────────────────────────────────┐
│              初次渲染 formA              │
│ currentTabComponent = FormA             │
│ keep-alive 渲染 FormA (created/mounted) │
└───────────────────────────────────────────┘
                ↓ 切换到 listB               
┌───────────────────────────────────────────┐
│ keep-alive deactivated FormA            │
│   (保存 FormA 状态)                      │
│ keep-alive 渲染 ListB (created/mounted)  │
└───────────────────────────────────────────┘
                ↓ 滚动 ListB               
┌───────────────────────────────────────────┐
│ ListB 滚动到 scrollTop = 150             │
│ deactivated 时保存 scrollTop             │
└───────────────────────────────────────────┘
                ↓ 切换回 formA               
┌───────────────────────────────────────────┐
│ keep-alive activated FormA               │
│   (恢复 FormA 表单数据)                   │
└───────────────────────────────────────────┘
                ↓ 再次切换到 listB           
┌───────────────────────────────────────────┐
│ keep-alive activated ListB               │
│   (恢复 scrollTop = 150)                 │
└───────────────────────────────────────────┘

8. 常见误区与注意事项

  1. 缓存与销毁的区别

    • 使用 keep-alive 后,不会触发组件的 destroyed 钩子,而是触发 deactivated。仅当组件真正从 keep-alive 范围之外移除,或 keep-alive 本身被销毁时才会触发 destroyed
  2. include/exclude 区分大小写

    • 传给 include/exclude 的值必须是组件的 name(注意区分大小写),而不能是文件名。
  3. 插槽与缓存

    • 如果子组件中有插槽,切换缓存不会影响插槽内容,但注意父传子时 props 的更新逻辑。
  4. 页面刷新后缓存失效

    • keep-alive 仅在内存中缓存组件状态,刷新页面会清空缓存。若需要持久化,可结合 localStorage/IndexedDB 保存必要状态。
  5. 第三方组件与缓存

    • 某些第三方组件(如轮播图)在第一次 mounted 后需要重新初始化,缓存后可能需要在 activated 中手动刷新数据或触发 update,否则可能出现显示异常。
  6. 多层 keep-alive 嵌套

    • 通常不建议多层嵌套,如果确实需要,要注意底层组件缓存优先级,较复杂场景下请仔细测试生命周期钩子触发。

9. 总结与最佳实践

  1. 使用场景

    • 多选项卡/多视图页面需要保持状态;
    • 路由切换时希望保留页面数据;
    • 大型表单/列表切换时避免重复请求和渲染。
  2. 核心配置

    • <keep-alive> 包裹需缓存的组件或 <router-view>
    • 借助 include/exclude 精准控制缓存范围;
    • 使用 max 限制缓存大小,避免内存飙升。
  3. 掌握生命周期钩子

    • activated:从缓存恢复时触发,可做数据刷新、滚动位置恢复;
    • deactivated:移出 DOM 时触发,可做状态保存、定时器销毁。
  4. 结合实际业务

    • 结合 vue-router 时,将 <router-view><keep-alive include="ViewName1,ViewName2"> 包裹,使指定路由组件缓存;
    • 对于列表组件,利用 activated 恢复滚动位置、选中项;对表单组件,保持输入内容。
  5. 性能优化

    • 对大数据量组件,注意初始加载逻辑,避免缓存时占用过多内存;
    • 避免一次性缓存过多不同组件,可通过设置 include 白名单或限定 max 大小。

通过本文对 keep-alive 的原理剖析、代码示例、ASCII 图解以及常见问题梳理,你已经掌握了在 Vue 项目中使用 keep-alive 组件保持状态的各种技巧。根据业务需求灵活运用,能够显著提升用户体验,让页面切换更加流畅自然。

VUE
最后修改于:2025年05月31日 12:11

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日