核心目标:组件切换时不卸载、不丢状态,实现“页面缓存效果”,并理解它在 React 中的实现边界。
本文以手写 KeepAlive为主线,从问题背景 → 原理拆解 → 核心实现 → 关键细节 → 与社区方案对比 → 面试深挖点,完整讲清楚这一能力。
一、为什么需要 KeepAlive?
在 React 中,条件渲染 = 卸载 + 重新挂载。
{activeTab === 'A' ? <A /> : <B />}
这会带来两个问题:
- 组件状态丢失(useState / useRef)
- 副作用重复执行(useEffect cleanup → re-run)
典型场景:
- 首页 / 列表页切到详情页再回来
- Tab 页面频繁切换
- 表单填写一半被打断
Vue 有 <keep-alive>,而 React 没有官方方案 —— 这正是考察React 运行时理解能力的地方。
二、KeepAlive 的本质是什么?
一句话总结:
不让组件从 React Fiber 树中消失,只是“看不见”
也就是:
- ❌ 不是重新渲染
- ❌ 不是重新挂载
- ✅ 只是
display: none
核心思想
- 组件只 mount 一次
- 用缓存结构保存组件实例
- 通过样式控制显隐,而不是条件渲染
三、实现思路拆解(非常重要)
1️⃣ 缓存容器:为什么用 Map / Object?
KeepAlive 的第一件事:
把已经渲染过的组件存起来
cache = {
A: <Counter />,
B: <OtherCounter />
}
- key:组件标识(activeId)
- value:ReactElement(JSX 本质)
面试补充:
Map支持任意 key(对象 / 函数)Object更轻量,适合 demo
2️⃣ 为什么 children 能被缓存?
<KeepAlive>
<Counter />
</KeepAlive>
children 是什么?
本质是一个 ReactElement 对象(普通 JS 对象)
只要:
- 不重新创建
- 不被卸载
React 内部 state 就能一直保留。
3️⃣ 切换的关键:display,而不是条件判断
错误做法 ❌:
{active && <Comp />}
正确做法 ✅:
<div style={{ display: active ? 'block' : 'none' }}>
<Comp />
</div>
display: none ≠ 卸载
四、完整手写 KeepAlive 实现
1️⃣ 使用示例(父组件)
const App = () => {
const [activeTab, setActiveTab] = useState('A');
return (
<div>
<button onClick={() => setActiveTab('A')}>A</button>
<button onClick={() => setActiveTab('B')}>B</button>
<KeepAlive activeId={activeTab}>
{activeTab === 'A'
? <Counter name="A" />
: <OtherCounter name="B" />}
</KeepAlive>
</div>
)
}
2️⃣ KeepAlive 核心实现
const KeepAlive = ({ activeId, children }) => {
const [cache, setCache] = useState({});
useEffect(() => {
if (!cache[activeId]) {
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>
))}
</>
)
}
五、关键 API 深度理解(面试高频)
1️⃣ Object.entries 是干嘛的?
Object.entries({ a: 1, b: 2 })
// => [['a', 1], ['b', 2]]
好处:
- 直接 map 渲染
- 解构
[key, value]非常语义化
2️⃣ 为什么 useEffect 依赖里有 children?
useEffect(() => {}, [activeId, children])
原因:
- children 是 ReactElement
- 不同组件切换时是新对象
- 需要感知变化,才能缓存
3️⃣ 为什么不能直接 setCache({...cache})?
因为:
- setState 是异步的
- 闭包可能拿到旧值
推荐写法:
setCache(prev => ({ ...prev, [key]: value }))
六、这种 KeepAlive 的局限性
必须说清楚,否则面试会被反杀。
❌ 1. 只是“视觉缓存”
- 组件仍然存在于 DOM
- 定时器、事件监听不会暂停
❌ 2. 无法精细控制生命周期
- 没有 activated / deactivated
- useEffect 不会重新触发
七、社区方案:react-activation
<AliveScope>
<KeepAlive id="home">
<Home />
</KeepAlive>
</AliveScope>
它解决了什么?
- Fiber 级缓存
- 提供 activated / deactivated
- 路由级 KeepAlive
但面试更爱问:
你知不知道它底层思想?
而不是:
“我用过这个库”
八、面试官最喜欢追问的 5 个问题
- React 为什么没有官方 keep-alive?
- display:none 和卸载的本质区别?
- children 为什么能直接缓存?
- useEffect 为什么不会再次执行?
- 如果组件很多,如何做 LRU 缓存?
如果你能顺着本文回答,中高级前端完全没问题。
九、总结一句话
KeepAlive 不是黑魔法,而是:缓存 ReactElement + 控制显示 + 不触发卸载
理解它,本质上是在理解:
- React 的渲染模型
- Fiber 生命周期
- JSX ≠ DOM