手写 React KeepAlive 组件:实现组件缓存与切换

35 阅读4分钟

在 React 开发中,组件的频繁挂载和卸载往往会导致状态丢失和性能开销,尤其在 tab 切换或路由场景下。例如,当用户在不同视图间切换时,如果组件每次都重新渲染,内部的计数器、表单数据等状态就会重置,用户体验变差。为了解决这个问题,React 官方提供了 <KeepAlive> 组件,但理解其原理并手写实现,能帮助我们更深入掌握组件生命周期管理和状态缓存。本文将基于一个简单的计数器 tab 切换示例,详细讲解 KeepAlive 的手写过程。我们会使用对象作为缓存结构(类似于 Map),通过 display 属性控制组件显示隐藏,从而实现缓存活化

KeepAlive 的核心原理

KeepAlive 的本质是缓存组件实例,避免卸载。它不像条件渲染(如 if 语句)那样销毁组件,而是将不活跃的组件“隐藏”起来,保留其状态。当切换回该组件时,直接显示即可,无需重新挂载。这涉及到 React 的渲染机制:组件可以通过 CSS 中 display: none 隐藏,但仍保持在 DOM 中,生命周期钩子不会触发卸载。

为什么用缓存结构?因为需要存储多个组件。例如,在多 tab 场景下,每个 tab 对应一个组件,我们用一个键值对存储:key 为 tab ID,value 为组件 JSX。Map(ES6 新增的数据结构),它允许任意类型作为 key(如对象),而对象字面量的 key 只能是字符串。但在简单场景下,对象 {} 也能胜任,且更轻量。JSON 与此类似,都是 key-value,但 JSON 是字符串序列化形式,不适合直接作为缓存。

切换显示的逻辑:当 activeId 变化时,检查缓存中是否有对应组件;如果没有,添加当前 children;然后遍历缓存,渲染所有组件,但只显示 active 的那个。通过 Object.entries 将缓存转为二维数组 [[key1, value1], [key2, value2]],便于 map 渲染。

这种设计提升了组件的复用性:父组件通过 children 传入任意内容,KeepAlive 只负责缓存和显示控制。props 如 activeId 用于标识当前活跃 tab。

示例场景:计数器 tab 切换

假设我们有一个 App 组件,包含两个计数器视图 A 和 B。用户点击按钮切换 tab,我们希望切换时保留计数器状态,而不是重置为 0。以下是 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'}}>
      <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'}}>
      <h3>{name} 视图</h3>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>点击加1</button>
    </div>
  )
}


const App = () => {
  const [activeTab, setActiveTab] = useState('A');

  return (
    <div>
      <div style={{marginBottom: '20px'}}>
        <button onClick={() => setActiveTab('A')}>显示A组件</button>
        <button onClick={() => setActiveTab('B')}>显示B组件</button>
      </div>
      {/* children 提升组件的定制能力 给父组件方便 */}
      <KeepAlive activeId={activeTab}>
        { activeTab === 'A' ? <Counter name="A" /> : <OtherCounter name="B" /> }
      </KeepAlive>
    </div>
  )
}

export default App

在这里,Counter 和 OtherCounter 是两个独立的计数器组件,使用 useEffect 记录挂载/卸载日志。App 通过 useState 管理 activeTab,按钮切换它。KeepAlive 包裹条件渲染的 children:当 activeTab 为 'A' 时渲染 Counter,否则 OtherCounter。

运行这个 App,如果没有 KeepAlive,切换 tab 时会看到控制台打印“卸载 A”和“挂载 B”,计数器重置。但有了 KeepAlive,只在首次渲染时挂载,以后切换只隐藏/显示,无卸载日志,状态保留。

手写 KeepAlive 组件

现在,我们实现 KeepAlive.jsx。它使用 useState 存储缓存,useEffect 在 activeId 或 children 变化时更新缓存。渲染时遍历缓存,应用 display 样式。代码如下:

import {
  useState,
  useEffect,
} from 'react';

const KeepAlive = ({
  activeId,
  children,
}) => {
  const [cache, setCache] = useState({});  // 缓存组件的
  // console.log(children, "--------");
  useEffect(() => {
    // activeId updata 切换显示
    // children updata 保存
    if (!cache[activeId]) {  // activeId key
      setCache((pre) => ({
        ...pre,
        [activeId]: children
      }))
    }
    // console.log(cache, "????????");
  }, [activeId, children, cache])
  return (
    <>
      {
        // Object.entries 对象变成数组
        // [key, value] 又方便使用
        Object.entries(cache).map(([id, components]) => (
          <div key={id} style={{display: id === activeId ? 'block' : 'none'}}
          >
            {components}
          </div>
        ))
      }
    </>
  )
}

export default KeepAlive

剖析代码:

  • useState({}) 初始化 cache 为空对象。
  • useEffect 依赖 [activeId, children, cache]:当这些变化时,检查 cache 是否有 activeId;如果没有,添加 children 到 cache。
  • 渲染部分:使用 Object.entries(cache) 转为 [[id, components]] 数组,map 渲染每个缓存项。每个项包裹在 div 中,key 为 id(确保唯一),style 根据 id === activeId 设置 display: block 或 none。
  • 注释解释了 Object.entries 的作用:将对象拆成易迭代的二维数组。

这个实现简单有效。首次切换到 'A' 时,cache 添加 {'A': },渲染 div display: block。切换 'B' 时,添加 {'B': },现在渲染两个 div:A 为 none,B 为 block。组件实例保留,状态不丢。

总结,手写 KeepAlive 考察了对 React 状态、effect 和渲染的理解。通过缓存和 display 控制,我们实现了高效组件复用。