写在前面
在 Vue 中,<keep-alive> 是一个内置的组件
用于缓存动态组件的状态,避免重复创建和销毁。
它常用于 <component :is="...">、<router-view> 等场景,实现“切换时不销毁组件实例”的效果。
我们知道 React 基于原生 JavaScript,具有更强的灵活性和定制能力,接下来我们将用 React 来实现这一机制。
一、组件的状态的丢失
在深入实现 KeepAlive 之前,我们可以先搭建一个简单场景,亲身体验一下:
当动态切换组件时,其内部状态为何会意外丢失。
编写一个带状态的计数器组件
我们需要一个具备本地状态的组件——例如一个计数器。在多个同类组件之间切换时,可以直观观察其状态是否被保留。
// src/App.jsx
import { useState, useEffect } from 'react';
import KeepAlive from './components/KeepAlive.jsx';
// 普通计数器组件
const Counter = ({ name }) => {
const [count, setCount] = useState(0);
//监听组件的挂载与卸载生命周期
useEffect(() => {
console.log(` $ {name} 组件已挂载!`);
return () => {
console.log(` $ {name} 组件已被卸载...`);
};
}, []);
return (
<div style={{ padding: '20px', border: '1px solid #ccc', margin: '10px' }}>
<h3>{name} 视图</h3>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>加 1</button>
</div>
);
};
// 另一个计数器组件(逻辑完全一致)
const OtherCounter = ({ name }) => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(` $ {name} 组件已挂载!`);
return () => {
console.log(` $ {name} 组件已被卸载...`);
};
}, []);
return (
<div style={{ padding: '20px', border: '1px solid #ccc', margin: '10px' }}>
<h3>{name} 视图</h3>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>加 1</button>
</div>
);
};
我们可以看到,尽管两个组件逻辑相同,但由于它们是不同的函数引用,React 会将其视为不同类型。因此,在条件渲染中切换时,React 会卸载旧组件并重新挂载新组件,导致状态丢失。
当我们通过路由或状态控制在这两个组件之间切换时,会发现:每次返回原组件,计数器都会重置为 0。
再深入一点, App.jsx 文件中,通过状态 activeTab 来控制显示哪一个组件。这样你可以体验到当组件被条件渲染时,默认情况下它们的状态是如何丢失的。
const App = () => {
// 使用 'A' 或 'B' 控制当前显示哪个组件
const [activeTab, setActiveTab] = useState('A');
return (
<div>
<div style={{ marginBottom: '20px' }}>
{/* 按钮用于切换 activeTab 状态 */}
<button onClick={() => setActiveTab('A')}>显示 A 组件</button>
<button onClick={() => setActiveTab('B')}>显示 B 组件</button>
</div>
{/* 注意:这里的三元运算符会导致组件在切换时被销毁和重建 */}
{activeTab === 'A' ? <Counter name="A" /> : <OtherCounter name="B" />}
</div>
);
};
我们运行这段代码时
- 点击“显示 A 组件”,然后多次点击“加 1”按钮,直到计数达到 5。
- 接着,点击“显示 B 组件”。此时,你应该会在控制台看到信息:“ A 组件已被卸载...” 和 “ B 组件已挂载!”。
- 最后,再次点击“显示 A 组件”。你会注意到控制台输出了“ A 组件已挂载!”,但遗憾的是,计数器已经重置为 0。
这种行为展示了 React 中条件渲染(的本质:每次当 activeTab 发生变化时,相应的组件会被卸载和重新挂载,导致其内部状态丢失。
而下一步,正是通过实现一个类似 Vue 中
<keep-alive>的KeepAlive机制,让组件在切换后仍能保留其内部状态与 DOM 结构,从而提升用户体验与性能。
二、:KeepAlive 的核心原理
既然 React 在条件切换时会销毁组件并清空状态,
那我们能不能换个思路——
不让组件被销毁,只让它“隐身”?
答案是肯定的。关键在于:
绕过 React 的卸载机制,用视觉隐藏代替真实移除。
2.1 核心思想
KeepAlive 的底层逻辑并不复杂,核心就两点:
所有需要缓存的子组件,一次性全部挂载到 DOM 中,永不卸载。
通过 CSS 的
display属性控制谁“露脸”、谁“退场”。
只要组件的真实节点还留在 DOM 树里(哪怕被设为 display: none),React 就不会触发它的卸载流程——
useState 的值依然保留
useEffect 的清理函数不会执行
整个组件实例在内存中“活着”,随时可以重新激活
这其实就是一种障眼法:
用户看到的是“切换视图”,而 React 看到的是“同一组组件,只是 visibility 变了”。
你不是在反复创建和销毁,而是在管理一组常驻组件的可见状态。既省去了重复初始化的开销,又完美保留了交互状态。
2.2 缓存结构设计
为了让这套机制可复用、可追踪,我们需要一个缓存容器,用来记录所有已经渲染过的子组件。
最直观的方式,就是用一个对象(或 Map)来维护:
const cache = {
A: <Counter name="A" />,
B: <OtherCounter name="B" />
};
Key:由外部传入的唯一标识(如 'A'、'B')决定,用于区分不同视图。
Value:对应的 React 元素(即 JSX 表达式),仅在首次访问时生成并存入缓存,后续直接复用。
虽然缓存的是 React 元素,但一旦它被挂载,其背后的状态、DOM 节点、Fiber 节点都会由 React 内部持续维护。只要不触发 unmount,一切状态都安然无恙。
最终渲染时,你只需遍历 cache,把所有缓存项都 render 出来,再根据当前激活的 key 动态设置 style={{ display: activeKey === key ? 'block' : 'none' }}。
看起来简单,对吧?
但要把它封装成一个通用、健壮、支持动态 children 的 <KeepAlive> 组件,还需要处理不少细节。接下来,我们就动手实现它。
三、手写实战:实现 KeepAlive 组件
现在,我们正式动手编写一个具备缓存能力的 KeepAlive 容器。
它将接管子组件的渲染逻辑,确保状态在切换中得以保留。
Props 与内部状态设计
我们的 KeepAlive 组件需要两个关键输入:
activeId:标识当前应显示的视图(如'A'、'B')。children:当前传入的子组件(即待缓存的 React 元素)。
同时,组件内部需维护一个缓存池,用于持久化所有已渲染过的子组件。
import { useState, useEffect } from 'react';
const KeepAlive = ({ activeId, children }) => {
// 缓存池:key 为视图 ID,value 为对应的 React 元素
const [cache, setCache] = useState({});
// ... 缓存与渲染逻辑
};
💡 注意:
children在 React 中本质是一个不可变的 React 元素(Element),将其存入状态是安全且合法的。
3.2 缓存策略:按需存储,避免重复
每当 activeId 或 children 发生变化时,我们需要判断:该视图是否首次出现?
如果是,则将其 children 存入缓存;否则,直接复用已有内容。
useEffect(() => {
// 仅当当前 activeId 对应的组件尚未缓存时,才进行存储
if (cache[activeId] == null) {
setCache(prev => ({
...prev,
[activeId]: children
}));
}
}, [activeId, children, cache]);
这里依赖
cache作为依赖项看似冗余,但能确保在严格模式(Strict Mode)或并发渲染下行为一致。实际项目中也可通过ref优化,但为保持逻辑清晰,此处暂用状态驱动。
3.3 渲染机制:全量挂载 + CSS 显隐
最关键的一步来了:不要用条件渲染!
我们要将所有已缓存的组件全部挂载到 DOM 中,仅通过 display: none/block 控制可见性。这样,React 不会触发卸载,状态自然得以保留。
return (
<>
{Object.entries(cache).map(([id, component]) => (
<div
key={id}
style={{
display: id === activeId ? 'block' : 'none',
// 可选:防止隐藏组件影响布局
position: 'absolute',
top: 0,
left: 0,
width: '100%'
}}
>
{component}
</div>
))}
</>
);
每个缓存项都始终存在于 DOM 树中,只是视觉上被隐藏。
React Fiber 树持续维护其状态,useState、useRef、useEffect均正常工作。
3.4 完整实现
整合上述逻辑,得到一个简洁而有效的 KeepAlive 组件:
import { useState, useEffect } from 'react';
const KeepAlive = ({ activeId, children }) => {
const [cache, setCache] = useState({});
useEffect(() => {
if (cache[activeId] == null) {
setCache(prev => ({
...prev,
[activeId]: children
}));
}
}, [activeId, children, cache]);
return (
<>
{Object.entries(cache).map(([id, component]) => (
<div
key={id}
style={{ display: id === activeId ? 'block' : 'none' }}
>
{component}
</div>
))}
</>
);
};
0export default KeepAlive;
这个不到 40 行的组件,巧妙利用了 React 的渲染机制与 CSS 的显示控制,实现了类似 Vue <keep-alive> 的缓存能力。
接下来,只需在 App.jsx 中替换原来的三元表达式,就能见证“失忆症”的彻底治愈。
我们来看看效果:
首先点击“显示 A 组件”,并将计数增加至 5。
随后切换至“显示 B 组件”。
此时观察控制台输出:没有出现“A 组件被卸载”的日志,仅打印了“B 组件已挂载”
这表明 A 组件并未被销毁,其对应的 useEffect 清理函数未被执行。
接着,重新切换回“显示 A 组件”。控制台未输出“A 组件已挂载” ,说明该组件实例未经历重新创建过程;同时,界面上的计数值依然保持为 5,状态完整保留。
这一结果验证了 KeepAlive 组件的核心能力:通过避免真实卸载、仅控制视觉显隐,成功实现了动态组件的状态持久化。组件在切换过程中维持了其内部状态、DOM 结构及生命周期上下文,达到了与 Vue 中 <keep-alive> 相似的效果。
总结
通过手写实现,我们成功在 React 中模拟了类似 Vue <keep-alive> 的状态缓存能力。但值得注意的是,两者的实现机制和底层逻辑存在根本性差异。
在 Vue 中,<keep-alive> 是一个深度集成于渲染器(renderer)的内置抽象组件。它通过操作虚拟 DOM 的 shapeFlag,直接干预组件的挂载(mount)与卸载(unmount)流程:被缓存的组件不会触发 beforeUnmount 和 unmounted,而是进入“停用”(deactivated)状态;重新激活时调用 activated 钩子,整个过程由 Vue 的响应式系统和组件生命周期体系原生支持。
而在 React 中,并无类似的底层钩子或渲染器扩展机制。因此,我们的实现本质上是一种上层应用级的“障眼法” :
- 所有子组件始终处于挂载状态,从未真正卸载;
- 通过 CSS 的
display: none实现视觉切换; - 依赖 React 对已挂载组件状态的自然保留能力来维持数据。
这意味着:
- 优势:实现简单,无需侵入 React 内部机制,兼容性好;
- 局限:所有缓存组件持续占用内存和 DOM 节点,可能影响性能(尤其在缓存大量复杂组件时);无法像 Vue 那样精确控制“激活/停用”生命周期(如暂停轮询、取消订阅等),需自行结合
useEffect与activeId状态管理副作用。
因此,React 版 KeepAlive 更适合轻量级、状态为主、副作用较少的场景;而 Vue 的 <keep-alive> 则提供了更精细的生命周期控制和更高效的内存管理策略。
归根结底:Vue 是“框架帮你管”,React 是“你自己想办法管” ——这也正是两种框架哲学差异的典型体现。