文章目录
- [一、KeepAlive 组件](#一、KeepAlive 组件)
- [二、AliveScope 容器](#二、AliveScope 容器)
- [三、useAliveController Hook](#三、useAliveController Hook)
- 四、生命周期
- 五、完整示例
react-activation 主要解决 React 项目中的「页面缓存」需求(是第三方库,非React 官方),类似于 Vue 中的 <KeepAlive>
:
功能 | 说明 |
---|---|
<KeepAlive> 组件 |
缓存组件 DOM,不卸载 |
<AliveScope> 容器 |
缓存容器,必须包裹在外层 |
useAliveController Hook |
提供缓存管理 API(如 drop 、refresh ) |
useActivate / useUnactivate 生命周期 |
激活与失活钩子 |
-
地址:GitHub 仓库
-
适配版本:推荐用于 React 16 和 React 17(React 18 存在一些副作用不稳定问题)
一、KeepAlive 组件
<KeepAlive>
是一个高阶组件,用于缓存(keep-alive)React 组件的状态,类似于 Vue 的 ,在组件卸载后保留其状态和 DOM,提升体验(例如表单不丢失、滚动位置保留等)。
用于缓存组件的 UI 和状态 ,当 <AdminHome />
页面切走再回来,状态不会丢失,组件不会重新挂载。
html
<KeepAlive id="admin-home">
<AdminHome />
</KeepAlive>
基本属性
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
id |
string |
- | 缓存唯一标识 ,必须唯一,一般用 pathname |
name |
string |
同 id |
可选的缓存名称(某些缓存操作可以用 name) |
when |
boolean |
true |
是否启用缓存,设为 false 表示不缓存 |
saveScrollPosition |
`false | 'screen' | 'container' |
autoFreeze |
boolean |
true |
切换时是否冻结 DOM,节省资源 |
extra |
any |
undefined |
附加信息,可在缓存控制器中使用(不常用) |
二、AliveScope 容器
<AliveScope>
是 react-activation
提供的作用域容器,用来管理缓存组件的上下文和分组控制。
-
提供上下文,让 KeepAlive 可以记录并复用组件状态
-
管理组件缓存生命周期
-
可用于分组销毁缓存(配合 dropScope(scopeId))
-
<AliveScope>
是必须包裹住所有<KeepAlive>
的组件,否则 KeepAlive 不会起作用。如果不包裹
<KeepAlive>
,它内部就无法访问缓存管理上下文:- KeepAlive 会直接按普通组件渲染,不会缓存
- useActivate / useUnactivate 钩子不会被调用
- useAliveController() 获取到的控制器是空的
html<AliveScope name="user-scope"> <KeepAlive id="user-list"><UserList /></KeepAlive> <KeepAlive id="user-detail"><UserDetail /></KeepAlive> </AliveScope>
标准写法是直接把
<AliveScope>
放在根节点。jsimport React from 'react'; import ReactDOM from 'react-dom'; import { AliveScope } from 'react-activation'; import App from './App'; ReactDOM.render( <AliveScope> <App /> </AliveScope>, document.getElementById('root') );
-
AliveScope 可选属性
属性 类型 默认值 说明 name
string
default
作用域唯一标识,可用于区分多个 AliveScope
include
string[]
[]
允许缓存的组件 ID 列表(白名单) exclude
string[]
[]
禁止缓存的组件 ID 列表(黑名单) max
number
Infinity
缓存数量上限,超过后会自动淘汰最久未使用的(LRU 策略) cacheKey
string
同 name
与 localStorage
缓存相关的 key(需要搭配persist
)persist
boolean
false
是否持久化缓存(刷新页面或关闭浏览器后,再次进入,缓存状态仍然保留,保存在 localStorage) autoClear
boolean
false
是否在页面刷新后自动清空缓存(防止缓存穿透,防止过时数据) -
配合 KeepAlive 使用
jsimport { AliveScope, KeepAlive } from 'react-activation'; import UserList from './UserList'; import UserDetail from './UserDetail'; export default function Root() { return ( <AliveScope name="user-scope" include={['user-list']}> <KeepAlive id="user-list"><UserList /></KeepAlive> <KeepAlive id="user-detail"><UserDetail /></KeepAlive> </AliveScope> ); }
-
只有 user-list 会被缓存(受 include 控制)
-
可以通过
useAliveController().dropScope('user-scope')
一键清除
-
-
多个 AliveScope 的使用方式
把用户模块和管理员模块的缓存完全隔离开,这样每个作用域有自己独立的缓存池
html<!-- 用户模块 --> <AliveScope name="user-scope"> <KeepAlive id="user-list"><UserList /></KeepAlive> <KeepAlive id="user-detail"><UserDetail /></KeepAlive> </AliveScope> <!-- 管理员模块 --> <AliveScope name="admin-scope"> <KeepAlive id="admin-home"><AdminHome /></KeepAlive> </AliveScope>
这时可以使用 useAliveController() 来获取缓存控制器,跨作用域控制:
jsimport { useAliveController } from 'react-activation'; export default function ClearButton() { const { dropScope } = useAliveController(); return ( <> <button onClick={() => dropScope('user-scope')}>清空用户模块缓存</button> <button onClick={() => dropScope('admin-scope')}>清空管理员模块缓存</button> </> ); }
-
dropScope('user-scope') 会销毁 user-scope 作用域中所有 KeepAlive 缓存
-
也可以用 refreshScope(name) 强制刷新一个作用域内所有组件
-
三、useAliveController Hook
这是一个自定义 Hook,提供对缓存组件的控制,比如手动刷新(drop)某个缓存组件、获取缓存状态等。
js
const { drop, dropScope, refresh } = useAliveController();
// 单作用域: 卸载某个缓存组件(通过 KeepAlive 的 id)
// 使用场景:点击"关闭标签页"
drop('user-list'); // 卸载 id 为 'user-list' 的 KeepAlive 缓存
drop('user-detail');
// 👉 全部要写一遍,或维护复杂缓存 id 列表
// 多作用域: 卸载该作用域下的所有缓存组件(通过 AliveScope 的 name),比 drop(id) 更高级别的操作
// 使用场景:退出登录清除所有缓存
dropScope('user-scope'); // 卸载 <AliveScope name="user-scope"> 下的所有 KeepAlive 缓存
// 强制刷新(先卸载再重建)
// 使用场景:点击"重置表单"或"刷新页面"
refresh('user-list'); // 会先 drop('user-list'),然后立刻重新挂载组件
-
dropScope 的参数是 中的 name。
-
使用前确保这些组件确实是包裹在
<AliveScope>
内的。 -
AliveScope 是 react-activation 中用于分组缓存的容器,必须明确设置 name 才能使用 dropScope。
.
🔥 在 react-activation 中,组件必须处于「非激活状态」( 即 KeepAlive 的 when 为 false、或组件被隐藏 ),才允许卸载或刷新。不会立即卸载当前正在显示的组件
方法名 | 作用说明 | 是否会立即卸载正在显示的组件 | 何时真正卸载组件 |
---|---|---|---|
drop(name) |
手动卸载指定缓存的组件(通过 name 标识) | ❌ 否 | 在组件被切换(即不再显示)后才真正卸载 |
dropScope(scope) |
卸载一整个作用域(scope 对应 <KeepAlive name="xxx" scope="xxx" /> )中的所有组件 |
❌ 否 | 同上:仅在这些组件不再显示时卸载 |
refresh(name) |
重新渲染指定组件(会先卸载再重新 mount) | ❌ 否 | 当前显示的组件会重建,但会保留在界面中刷新(不是立即卸载) |
refreshScope(scope) |
刷新某个 scope 下的所有组件 | ❌ 否 | 当前显示的组件会重建(看起来是刷新) |
clear() |
清空所有缓存组件 | ❌ 否 | 当前显示的组件仍保留,其他会被清空(切走后卸载) |
getCachingNodes() |
获取当前缓存中的组件节点列表 | - | - |
-
drop / dropScope / refresh / refreshScope
不会卸载当前正在显示的组件 -
它们只对非激活(未渲染)的组件生效
-
✅ 正确的做法是:切换走 → drop → 才生效:
jshistory.push('/other'); await drop('/current'); // ✅ 现在它处于非激活状态,drop 生效
四、生命周期
react-activation (React 第三方库) 提供的自定义 Hook,模拟 Vue 的 activated / deactivated 生命周期。
js
<AliveScope>
<KeepAlive id="user">
<UserPage />
</KeepAlive>
</AliveScope>
import { useActivate, useUnactivate, useAliveEffect } from 'react-activation';
// 组件激活时调用(进入或返回该缓存组件时),替代 useEffect 的 didShow
useActivate(() => {
console.log('页面被激活(显示): 进入时刷新数据')
});
// 组件失活时调用(从该组件跳出,但未卸载),类似 componentWillPause
useUnactivate(() => {
console.log('页面被隐藏但未卸载: 退出时保存状态')
});
// 只有当组件是"激活状态"时,才会执行 useEffect,可以响应 deps 的变化,可替代 useEffect + useActivate 组合
useAliveEffect(() => {
const timer = setInterval(() => {
console.log('只在激活状态时轮询');
}, 1000);
return () => clearInterval(timer);
}, []);
类似于 Vue3:
js
<template>
<KeepAlive include="UserPage">
<component :is="currentView" />
</KeepAlive>
</template>
// 原生生命周期钩子
onActivated(() => {
console.log('组件被缓存激活');
});
onDeactivated(() => {
console.log('组件被缓存关闭');
});
五、完整示例
✅ 标签切换自动缓存
✅ 点击关闭标签页 → 销毁对应缓存
✅ 支持多个 AliveScope 管理模块分组
✅ 使用 KeepAlive + useActivate + useUnactivate
-
main.tsx(注册多个作用域)
jsimport React from 'react'; import ReactDOM from 'react-dom'; import { AliveScope } from 'react-activation'; import App from './App'; ReactDOM.render( <> <AliveScope name="module-user"> <App /> </AliveScope> {/* 可拓展其他模块作用域 */} </>, document.getElementById('root') );
-
App.tsx(入口,渲染标签页)
jsimport React from 'react'; import TabView from './components/TabView'; export default function App() { return ( <div> <TabView /> </div> ); }
-
TabView.tsx(核心组件)
jsimport React, { useState } from 'react'; import { KeepAlive, useAliveController } from 'react-activation'; import PageA from './PageA'; import PageB from './PageB'; import PageC from './PageC'; const tabComponents: Record<string, React.ReactNode> = { A: <PageA />, B: <PageB />, C: <PageC />, }; const TabView = () => { const [tabs, setTabs] = useState(['A', 'B', 'C']); const [active, setActive] = useState('A'); const { drop } = useAliveController(); const closeTab = async (key: string) => { // 比如当前 tabs 是 ['A', 'B', 'C'],要关闭 A 标签 setTabs(prev => prev.filter(t => t !== key)); // 更新标签页列表(异步),由['A', 'B', 'C'] -> ['B', 'C'] if (active === key) { // 如果关闭的是当前激活标签 const other = tabs.find(t => t !== key); // 从标签页列表['A', 'B', 'C']中找出第一个非 key 的 tab(即 'B') if (other) setActive(other); // 激活新标签B } await drop(`page-${key}`); // 卸载对应标签的缓存组件(卸载'page-A') }; return ( <div> <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}> {tabs.map(tab => ( <div key={tab} style={{ padding: '6px 12px', border: active === tab ? '2px solid blue' : '1px solid #ccc', borderRadius: 4, cursor: 'pointer', background: '#f7f7f7', position: 'relative', }} onClick={() => setActive(tab)} > Page {tab} <span onClick={e => { e.stopPropagation(); closeTab(tab); }} style={{ marginLeft: 6, color: 'red', cursor: 'pointer', fontWeight: 'bold', }} > × </span> </div> ))} </div> <div style={{ border: '1px solid #ccc', padding: 12 }}> {tabs.map(tab => active === tab ? ( <KeepAlive id={`page-${tab}`} key={tab}> {tabComponents[tab]} </KeepAlive> ) : null )} </div> </div> ); }; export default TabView;
-
PageA.tsx(缓存与生命周期)
jsimport React, { useState } from 'react'; import { useActivate, useUnactivate } from 'react-activation'; export default function PageA() { const [count, setCount] = useState(0); useActivate(() => { console.log('PageA 激活'); }); useUnactivate(() => { console.log('PageA 失活'); }); return ( <div> <h2>Page A</h2> <p>计数: {count}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> </div> ); }
PageB.tsx、PageC.tsx 同上