我真的写了个“诈尸式”缓存组件:手撕React KeepAlive

别让路由切换,变成组件的"告别仪式"


从一次"页面回魂"需求说起

产品经理走过来,拍了拍我的肩膀:

"用户从首页进详情,再返回首页,希望滚动位置还在原来那里,表单填写的内容也别丢。"

我看了眼代码------react-router 一切换路由,组件直接 unmount,DOM 连渣都不剩。

这哪是返回首页,这是让首页重新投胎

于是我开始思考:能不能让某些组件假死------看起来没了,实际上藏在内存里,等需要时再"还魂"?

这种模式,在 Vue 里叫 <KeepAlive>,在 React 里......没有官方实现。

那我干脆手写一个。


为什么 React 没有内置 KeepAlive?

Vue 的 <KeepAlive> 基于其模板编译器和组件实例化机制,可以轻松控制组件是否销毁。

而 React 的哲学是 "UI = f(state)",组件销毁/重建是常态,缓存反而破坏了"单向数据流"的纯粹性。

但现实业务不跟你讲哲学------高频切换的 Tab、详情页返回列表、表单分步填写,用户根本不关心你的渲染模式,他们只关心:

"我刚才填的东西怎么没了?"

所以,我们需要自己造轮子


核心思路:不卸载,只隐藏

React 组件卸载是因为 DOM 被移除。

那我们换个做法:

  • 首次渲染:正常挂载 DOM
  • 切换离开 :不卸载,只把 display 设为 none
  • 切换回来 :把 display 改回 block

这样组件实例一直存在,stateref、滚动位置全部保留。

听起来简单,但实现时有两个关键点:

  1. 渲染容器必须独立于父组件,不能随父组件 re-render 而丢失
  2. 渲染逻辑要区分"首次挂载"和"后续显示/隐藏"

手写实现:从 0 到 1 的 KeepAlive

直接上完整代码(基于你提供的版本,我做了增强和注释):

jsx 复制代码
import {
    useEffect,
    useRef
} from 'react';
import ReactDOM from 'react-dom';

export default function KeepAlive({ active, children }) {
    const containerRef = useRef(null);
    const contentRef = useRef(null);
    useEffect(() => {
        // 挂载
        if(!contentRef.current) {
            contentRef.current = document.createElement('div');
            containerRef.current.appendChild(contentRef.current);
            ReactDOM.render(children, contentRef.current);
        }
        return () => {
            contentRef.current.style.display = 'none';
        }
    }, []);
    useEffect(() => {
        // 更新
        if(contentRef.current) {
            if(active) {
                contentRef.current.style.display = 'block';
            } else {
                contentRef.current.style.display = 'none';
            }
        }
    }, [active]);
    return (
        <div ref={containerRef}>
            {children}
        </div>
    );
}

使用示例

tsx 复制代码
import { useState } from 'react';
import KeepAlive from './KeepAlive';

export default function App() {
  const [tab, setTab] = useState<'home' | 'about'>('home');

  return (
    <div>
      <button onClick={() => setTab('home')}>首页</button>
      <button onClick={() => setTab('about')}>关于</button>

      <KeepAlive active={tab === 'home'}>
        <HomePage />
      </KeepAlive>

      <KeepAlive active={tab === 'about'}>
        <AboutPage />
      </KeepAlive>
    </div>
  );
}

activefalse 时,组件 DOM 隐藏但实例保留,状态完整。


避坑指南:我踩过的 3 个坑

坑 1:ReactDOM.render 已经废弃?

在 React 18 中,ReactDOM.render 被标记为 Legacy。

如果你用 React 18+,建议改用 createRoot

tsx 复制代码
import { createRoot } from 'react-dom/client';

// 在首次挂载时
const root = createRoot(div);
root.render(<>{children}</>);
// 记得保存root实例以便后续更新

坑 2:children 变化时怎么更新?

上面的实现只在首次挂载时渲染一次 children。如果 children 本身会变(比如传入了不同组件),你需要额外处理:

tsx 复制代码
useEffect(() => {
  if (contentRef.current) {
    // 重新渲染更新内容
    root.render(<>{children}</>);
  }
}, [children]);

但这样会丢失子组件内部状态,所以 KeepAlive 通常只缓存同一个组件实例

坑 3:多个 KeepAlive 同时存在会怎样?

每个 KeepAlive 都会创建独立的 DOM 容器,互不干扰。但要注意:

  • 如果同时缓存 10 个大型组件,内存占用会上升
  • 建议配合 LRU 策略,只缓存最近 N 个

进阶:支持"销毁"和"恢复"生命周期

Vue 的 KeepAlive 提供了 activated / deactivated 钩子。

我们可以在 React 中模拟:

tsx 复制代码
useEffect(() => {
  if (active) {
    // 组件被激活(从隐藏变为显示)
    console.log('activated');
  } else {
    // 组件被停用(从显示变为隐藏)
    console.log('deactivated');
  }
}, [active]);

结合 useMemouseCallback 传递回调,就能让子组件感知缓存状态。


对比 Vue 的 KeepAlive:我们差在哪里?

特性 Vue 内置 手写实现
缓存实例 ✅ 自动 ✅ 手动控制
最大缓存数 ✅ LRU ❌ 需自己实现
条件排除 ✅ include/exclude ❌ 需业务层判断
生命周期钩子 ✅ activated/deactivated ⚠️ 可模拟
内存回收 ✅ 自动 ⚠️ 需手动管理

我们的版本是"最小可用原型",适合理解原理,生产环境建议封装更完善的库。


实战场景:哪些地方非用不可?

  1. Tab 切换 (订单列表/商品列表)

    用户切换 Tab 后返回,不想重新加载数据

  2. 多步骤表单 (分步注册)

    用户从第 3 步跳到第 1 步,再回来时第 3 步内容还在

  3. 视频/音频播放器

    切换页面后继续播放,不中断

  4. 地图组件

    重新加载地图会重置视角和标记


性能考量:缓存不是银弹

  • 内存 :每个缓存的组件都保留完整 DOM 和 JS 实例,大型列表页慎用
  • 更新:隐藏的组件不会触发 re-render,但事件监听仍然存在
  • 最佳实践 :只缓存 "高频切换且构建成本高" 的组件

结语:从"轮子"到"理解"

手写 KeepAlive 最大的收获,不是写出了一个可用的组件,而是彻底理解了 React 渲染机制和 DOM 操作边界

  • 原来组件不是非得卸载
  • 原来 DOM 可以"寄生"在任意容器里
  • 原来 useRef 可以跨渲染保存实例

下次产品再提"保留状态"的需求,你不会再头皮发麻------

你会微微一笑,掏出自己的 KeepAlive,然后说:

"放心,我让组件原地复活,不投胎。"

相关推荐
A24207349301 小时前
Vue.js 初学者注意事项与项目开发实践指南
前端·javascript·vue.js
用户059540174461 小时前
「记仇」记忆存储测试踩坑实录:用 Pytest+Docker 把 BUG 扼杀在凌晨 3 点前
前端·css
dllmayday2 小时前
Bootstrap三开关样式
前端·javascript·bootstrap
杨先生哦2 小时前
【2026热端攻防系列 11/12】前端AI风控攻防实战:验证码缺陷、人机验证绕过、智能爬虫对抗与企业智能风控加固方案
前端·人工智能·笔记·爬虫·安全
CodeSheep2 小时前
hvv员工家属爆料:老公22年入职的,普通员工,老是被误以为高薪多奖金,实际上还没配股,暂时钱没存多少,我倒是很心疼他工作辛苦。
前端·后端·程序员
IT_陈寒2 小时前
Redis这个内存杀手坑惨了我,排查三小时发现是过期策略搞的鬼
前端·人工智能·后端
Lzh编程小栈2 小时前
【STM32底层精讲】RCC时钟系统超全详解(时钟树+源码+避坑指南)
c语言·stm32·单片机·嵌入式硬件·面试
人间凡尔赛2 小时前
React Compiler 正式落地一年:告别手动 useMemo/useCallback 的全栈实践
前端·性能优化·react
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-08-02
前端·神经网络·react.js·搜索引擎·前端框架