手写 React KeepAlive:原理、实现与面试深挖

11 阅读3分钟

核心目标:组件切换时不卸载、不丢状态,实现“页面缓存效果”,并理解它在 React 中的实现边界。

本文以手写 KeepAlive为主线,从问题背景 → 原理拆解 → 核心实现 → 关键细节 → 与社区方案对比 → 面试深挖点,完整讲清楚这一能力。


一、为什么需要 KeepAlive?

在 React 中,条件渲染 = 卸载 + 重新挂载

{activeTab === 'A' ? <A /> : <B />}

这会带来两个问题:

  1. 组件状态丢失(useState / useRef)
  2. 副作用重复执行(useEffect cleanup → re-run)

典型场景:

  • 首页 / 列表页切到详情页再回来
  • Tab 页面频繁切换
  • 表单填写一半被打断

Vue 有 <keep-alive>,而 React 没有官方方案 —— 这正是考察React 运行时理解能力的地方。


二、KeepAlive 的本质是什么?

一句话总结:

不让组件从 React Fiber 树中消失,只是“看不见”

也就是:

  • ❌ 不是重新渲染
  • ❌ 不是重新挂载
  • ✅ 只是 display: none

核心思想

  1. 组件只 mount 一次
  2. 用缓存结构保存组件实例
  3. 通过样式控制显隐,而不是条件渲染

三、实现思路拆解(非常重要)

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 个问题

  1. React 为什么没有官方 keep-alive?
  2. display:none 和卸载的本质区别?
  3. children 为什么能直接缓存?
  4. useEffect 为什么不会再次执行?
  5. 如果组件很多,如何做 LRU 缓存?

如果你能顺着本文回答,中高级前端完全没问题


九、总结一句话

KeepAlive 不是黑魔法,而是:缓存 ReactElement + 控制显示 + 不触发卸载

理解它,本质上是在理解:

  • React 的渲染模型
  • Fiber 生命周期
  • JSX ≠ DOM