文章目錄
- 一、KeepAlive 組件
- 二、AliveScope 容器
- 三、useAliveController Hook
- 四、生命周期
- 五、完整示例
react-activation 主要解決 React 項目中的「頁面緩存」需求(是第三方庫,非React 官方),類似于 Vue 中的 <KeepAlive>
:
功能 | 說明 |
---|---|
<KeepAlive> 組件 | 緩存組件 DOM,不卸載 |
<AliveScope> 容器 | 緩存容器,必須包裹在外層 |
useAliveController Hook | 提供緩存管理 API(如 drop 、refresh ) |
useActivate / useUnactivate 生命周期 | 激活與失活鉤子 |
-
地址:GitHub 倉庫
-
NPM:https://www.npmjs.com/package/react-activation
-
適配版本:推薦用于 React 16 和 React 17(React 18 存在一些副作用不穩定問題)
一、KeepAlive 組件
<KeepAlive>
是一個高階組件,用于緩存(keep-alive)React 組件的狀態,類似于 Vue 的 ,在組件卸載后保留其狀態和 DOM,提升體驗(例如表單不丟失、滾動位置保留等)。
用于緩存組件的 UI 和狀態,當 <AdminHome />
頁面切走再回來,狀態不會丟失,組件不會重新掛載。
<KeepAlive id="admin-home"><AdminHome />
</KeepAlive>
基本屬性
屬性 | 類型 | 默認值 | 說明 |
---|---|---|---|
id | string | - | 緩存唯一標識,必須唯一,一般用 pathname |
name | string | 同 id | 可選的緩存名稱(某些緩存操作可以用 name) |
when | boolean | true | 是否啟用緩存,設為 false 表示不緩存 |
saveScrollPosition | false | 'screen' | 'container' | ((node: HTMLElement) => any) | false | 是否保存并恢復頁面的滾動位置:false (默認):不保存滾動位置 'screen' : 保存并恢復頁面(window)滾動位置 'container' : 自動尋找最近的滾動容器保存滾動位置 (node) => HTMLElement : 自定義返回要記錄滾動位置的 DOM 元素 |
autoFreeze | boolean | true | 切換時是否凍結 DOM,節省資源 |
extra | any | undefined | 附加信息,可在緩存控制器中使用(不常用) |
二、AliveScope 容器
<AliveScope>
是 react-activation
提供的作用域容器,用來管理緩存組件的上下文和分組控制。
-
提供上下文,讓 KeepAlive 可以記錄并復用組件狀態
-
管理組件緩存生命周期
-
可用于分組銷毀緩存(配合 dropScope(scopeId))
-
<AliveScope>
是必須包裹住所有<KeepAlive>
的組件,否則 KeepAlive 不會起作用。如果不包裹
<KeepAlive>
,它內部就無法訪問緩存管理上下文:- KeepAlive 會直接按普通組件渲染,不會緩存
- useActivate / useUnactivate 鉤子不會被調用
- useAliveController() 獲取到的控制器是空的
<AliveScope name="user-scope"><KeepAlive id="user-list"><UserList /></KeepAlive><KeepAlive id="user-detail"><UserDetail /></KeepAlive> </AliveScope>
標準寫法是直接把
<AliveScope>
放在根節點。import 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 使用
import { 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 的使用方式
把用戶模塊和管理員模塊的緩存完全隔離開,這樣每個作用域有自己獨立的緩存池
<!-- 用戶模塊 --> <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() 來獲取緩存控制器,跨作用域控制:
import { 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)某個緩存組件、獲取緩存狀態等。
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(id) | 刪除某個緩存組件的狀態 | ? 不會立即卸載當前正在顯示的 | ?? 當該組件被切換隱藏時 |
dropScope(scopeId) | 刪除整個 AliveScope 中的緩存 | ? 不會立即卸載當前正在顯示的 | ?? 當前組件不顯示后才會銷毀 |
refresh(id) | 刪除后重新創建組件 | ? 不會立即刷新當前激活組件 | ?? 必須切到其他組件再切回來才生效 |
-
drop / dropScope / refresh
不會卸載當前正在顯示的組件 -
它們只對非激活(未渲染)的組件生效
-
? 正確的做法是:切換走 → drop → 才生效:
history.push('/other'); await drop('/current'); // ? 現在它處于非激活狀態,drop 生效
四、生命周期
react-activation(React 第三方庫) 提供的自定義 Hook,模擬 Vue 的 activated / deactivated 生命周期。
<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:
<template><KeepAlive include="UserPage"><component :is="currentView" /></KeepAlive>
</template>// 原生生命周期鉤子
onActivated(() => {console.log('組件被緩存激活');
});onDeactivated(() => {console.log('組件被緩存關閉');
});
五、完整示例
? 標簽切換自動緩存
? 點擊關閉標簽頁 → 銷毀對應緩存
? 支持多個 AliveScope 管理模塊分組
? 使用 KeepAlive + useActivate + useUnactivate
-
main.tsx(注冊多個作用域)
import 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(入口,渲染標簽頁)
import React from 'react'; import TabView from './components/TabView';export default function App() {return (<div><TabView /></div>); }
-
TabView.tsx(核心組件)
import 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 => (<divkey={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}<spanonClick={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(緩存與生命周期)
import 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 同上