
一句话:
<Activity>(内部代号 Offscreen)解决的是「既要隐藏、又要保留状态、还要别浪费资源」这个三方矛盾——它在mode="hidden"时会「卸载副作用(清理清理函数)但保留 state」,而不是像条件渲染那样把整个组件扔掉。
一、三种「隐藏」方式的真实差异
// ① 条件渲染(最常见的写法)
{tab === 'profile' && <ProfilePanel />}
// ② CSS 隐藏
<div style={{ display: tab === 'profile' ? 'block' : 'none' }}>
<ProfilePanel />
</div>
// ③ Activity(React 19.2 起;此前是内部代号 Offscreen)
<Activity mode={tab === 'profile' ? 'visible' : 'hidden'}>
<ProfilePanel />
</Activity>
| 维度 | 条件渲染 | CSS 隐藏 | Activity |
|---|---|---|---|
| 组件 state | ❌ 丢失(卸载重建) | ✅ 保留 | ✅ 保留 |
| DOM 节点 | ❌ 移除 | ✅ 保留 | ✅ 保留(但不可见) |
| 副作用(定时器/订阅/请求) | ✅ 随卸载清理 | ❌ 继续跑(浪费) | ✅ 暂停(执行清理) |
| 重显时的成本 | 重建(重新取数、重新挂载) | 极低(只是改样式) | 极低(复用 state,只重建 effect) |
| 布局/绘制 | 不参与 | 不参与(display:none 会被浏览器跳过) |
不参与 |
| 适合 | 一次性内容、状态不需要保留 | 保留状态且「副作用无所谓」 | 保留状态 + 副作用需要暂停 |
⭐⭐ 三种方式的本质区别(一张图记住):
条件渲染 = 【销毁一切】:state ❌ DOM ❌ effect ❌
CSS 隐藏 = 【全都不动】:state ✅ DOM ✅ effect ✅(继续跑,浪费)
Activity = 【保留数据、暂停行为】:state ✅ DOM ✅ effect ⏸️(清理但不丢 state)
⭐ 为什么「CSS 隐藏」不够好?——具体浪费在哪:
① 轮询还在发请求(用户切走了 Tab,请求还在打)
② WebSocket 还在收消息(消息洪水)
③ setInterval 还在跑(每秒一次无意义的 setState → 触发无意义的渲染)
④ 动画还在耗 CPU(虽然不可见,但 JS 驱动的动画仍在计算)
⑤ ResizeObserver / IntersectionObserver 还在回调
⑥ 媒体(video/audio)可能继续解码
→ 结论:CSS 隐藏「省了 DOM 重建」,但「没省运行时资源」。
Activity 的真正价值是「把运行时资源也停下来」。
二、Activity 的语义与 API
import { Activity } from 'react'; // ⚠️ 截至 React 19.2 可用;以官方文档为准
<Activity mode="visible">…</Activity> // 正常显示、effects 正常运行
<Activity mode="hidden">…</Activity> // 隐藏:卸载 effects,保留 state 与 DOM
⭐⭐ hidden 时到底发生了什么(四条,按重要性排序):
① 【执行所有 Effect 的清理函数】
→ 订阅取消、定时器清除、请求 abort、Observer disconnect
→ 与「组件卸载」相同的清理流程(但不销毁 state)
② 【保留组件 state】
→ 下一次 visible 时,useState 的值还在(不需要重新取数)
③ 【保留 DOM 节点,但不可见】
→ React 会在内部容器上设置 display: none(或等价的隐藏)
→ 因此「布局与绘制」被浏览器跳过(不产生视觉成本)
④ 【可以「预渲染」一个「从未显示过」的子树】
→ 这就是「后台准备下一步页面」的能力:先渲染好,等到 visible 时立刻可交互
→ 官方把这种模式叫 prerendering
⭐⭐ 与 Suspense 的关系(面试常问):
Suspense 内部就是用 Offscreen/Activity 实现的——
当子树「挂起」时,React 需要:
· 把「已经渲染出来的部分」隐藏(而不是删掉)
· 等数据就绪后再显示
→ 这正是「保留 DOM + 隐藏」的语义。
⭐ 所以可以说:Activity 是「Suspense 的底层能力公开化」。
(反过来,`mode="hidden"` 的子树在「后台预渲染」时也会用到 Suspense 的机制)
⭐ 内部实现思路(React 源码里的关键概念,供理解用):
· Offscreen fiber:副作用的标记里有一个专门的 tag(`Offscreen`)
· `hideOrUnhideAllChildren`:递归地把子树「隐藏/显示」(改 style 或加 hidden 属性)
· 「隐藏时」:React 会「遍历子树并执行 effect 的 cleanup」(因为要靠 cleanup 停掉资源)
· 「显示时」:重新执行 effect 的 create(等价于一次「没有重新挂载 state 的挂载」)
· 「预渲染」:用「不会提交到屏幕」的方式先跑一遍(类似 Suspense 的「离屏渲染」)
⚠️ 这些是实现细节、会随版本变化;理解语义比记函数名重要。
三、使用场景(四类)
// ✅ 场景一:Tab / 步骤切换,保留每个 Tab 的状态与滚动位置
function SettingsTabs() {
const [active, setActive] = useState('profile');
return (
<>
<TabNav active={active} onChange={setActive} />
<Activity mode={active === 'profile' ? 'visible' : 'hidden'}>
<ProfileForm /> {/* 表单草稿、滚动位置都会被保留 */}
</Activity>
<Activity mode={active === 'security' ? 'visible' : 'hidden'}>
<SecurityPanel /> {/* 但「不可见时」它的订阅/轮询会停掉 */}
</Activity>
</>
);
}
// ✅ 场景二:预渲染「下一步页面」(后台准备好,切换时秒开)
function Wizard({ step }: { step: number }) {
return (
<>
<Activity mode={step === 1 ? 'visible' : 'hidden'}><Step1 /></Activity>
<Activity mode={step === 2 ? 'visible' : 'hidden'}><Step2 /></Activity>
{/* ⭐ 两个 Step 一开始都会被预渲染(hidden 的那个在后台准备),
用户点「下一步」时立刻可交互(不需要等待取数) */}
</>
);
}
// ✅ 场景三:Modal / Drawer 的「关闭但保留状态」
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>打开编辑器</button>
<Activity mode={open ? 'visible' : 'hidden'}>
<HeavyEditor /> {/* 关闭时:编辑器实例的订阅暂停,但文档内容与光标位置保留 */}
</Activity>
</>
);
}
// ✅ 场景四:路由级(框架内部用)
// 路由切换时:把「即将离开的页面」设为 hidden(保留状态,供「返回」时秒开),
// 把「即将进入的页面」设为 visible(并可能先 hidden 预渲染)
// ⭐ 这正是「返回上一页时表单内容与滚动位置还在」的底层能力
⭐ 判断「该不该用 Activity」的两个问题:
① 隐藏后,用户「大概率会回到」这个状态吗?
├─ 会(Tab 返回、Modal 重开、路由后退)→ 值得保留 state
└─ 不会(一次性弹窗、走完的流程)→ 用条件渲染(卸载更省内存)
② 它的副作用「在隐藏期间继续跑」会有代价吗?
├─ 有(轮询、WebSocket、定时器、动画)→ Activity 的「暂停 effect」有实际收益
└─ 没有(纯静态展示)→ CSS 隐藏就够了,但 Activity 也不亏
四、完整实现:现在就能用的等价方案(170 行)
真正让「子树里的 useEffect 自动暂停」需要 React 内部支持。但我们可以用一个遵循约定的等价方案做到 90% 的效果:用 Context 广播「可见性」,并提供 useActivityEffect 让副作用可暂停。
// activity.tsx —— Activity 的等价实现(React 18/19 都可用)
import {
createContext, useContext, useEffect, useRef,
type ReactNode, type DependencyList, type EffectCallback,
} from 'react';
/** 当前子树是否「活跃」(可见) */
const ActivityContext = createContext(true);
/** 读取「当前是否活跃」——供自定义 Hook 使用 */
export function useIsActive(): boolean {
return useContext(ActivityContext);
}
/**
* ⭐ 核心:一个「可暂停」的 Effect
* · active = true → 正常执行(并在依赖变化时重建)
* · active = false → 执行清理函数(相当于「暂停」)
* · 恢复 active → 重新执行
*
* ⚠️ 注意:它与真正的 Activity 有一个关键差别——
* 真正的 Activity 由 React 自动处理「子树里所有 useEffect」;
* 这里需要「子组件主动用 useActivityEffect」才会暂停。
*/
export function useActivityEffect(
effect: EffectCallback,
deps: DependencyList = [],
options: { alsoWhenActive?: boolean } = {}
) {
const active = useIsActive();
useEffect(() => {
if (!active) return undefined; // ⭐ 不活跃 → 不建立副作用
return effect(); // 活跃 → 建立(并返回清理函数)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, active]);
}
/** 一个「暂停时也保留 state」的定时器(用 useActivityEffect 实现) */
export function useActivityInterval(callback: () => void, delay: number | null) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
useActivityEffect(() => {
if (delay === null) return undefined;
const id = setInterval(() => callbackRef.current(), delay);
return () => clearInterval(id); // ⭐ 不可见时会执行这里 → 定时器停掉
}, [delay]);
}
/** 一个「暂停时取消订阅」的轮询(演示资源节省) */
export function useActivityPolling<T>(fetcher: () => Promise<T>, delay: number, onData: (d: T) => void) {
const onDataRef = useRef(onData);
useEffect(() => {
onDataRef.current = onData;
});
useActivityEffect(() => {
let cancelled = false;
let timer: ReturnType<typeof setTimeout>;
const tick = async () => {
try {
const data = await fetcher();
if (!cancelled) onDataRef.current(data);
} finally {
if (!cancelled) timer = setTimeout(tick, delay);
}
};
tick();
return () => {
cancelled = true;
clearTimeout(timer); // ⭐ 不可见时停止轮询
};
}, [fetcher, delay]);
}
/** ============ Activity 组件本体 ============ */
interface ActivityProps {
mode: 'visible' | 'hidden';
children: ReactNode;
/** 隐藏时是否同时从 DOM 树中移除(默认 false = 只隐藏) */
unmountOnHidden?: boolean;
}
export function Activity({ mode, children, unmountOnHidden = false }: ActivityProps) {
const active = mode === 'visible';
const hostRef = useRef<HTMLDivElement>(null);
/**
* ⭐ 关键细节:用「DOM 属性 + ref」来隐藏,
* 而不是「条件渲染」——这样 state 与 DOM 都保留下来。
*/
useEffect(() => {
const el = hostRef.current;
if (!el) return;
// hidden 属性会让浏览器「跳过布局与绘制」(与 display:none 等价)
el.hidden = !active;
}, [active]);
if (unmountOnHidden && !active) return null; // 「真卸载」模式(不保留 state)
return (
<ActivityContext.Provider value={active}>
<div ref={hostRef} hidden={!active} style={{ display: 'contents' }}>
{/* ⚠️ display: contents 让这个包裹层「不影响布局」,同时仍可挂 hidden 属性
(如果你的项目不支持 display:contents,改成 display:block/none 也可以) */}
{children}
</div>
</ActivityContext.Provider>
);
}
/** ============ 使用示例:一个「保留状态 + 暂停副作用」的 Tab 容器 ============ */
import { useState } from 'react';
import { Activity, useActivityInterval, useActivityPolling } from './activity';
function LiveCounter({ label }: { label: string }) {
const [seconds, setSeconds] = useState(0);
// ⭐ 用「可暂停的定时器」——Tab 不可见时它不会跑
useActivityInterval(() => setSeconds((s) => s + 1), 1000);
return <p>{label}:已运行 {seconds} 秒</p>;
}
function StockTicker({ symbol }: { symbol: string }) {
const [price, setPrice] = useState<number | null>(null);
// ⭐ 用「可暂停的轮询」——Tab 不可见时不会发请求
useActivityPolling(
() => fetch(`/api/quote?symbol=${symbol}`).then((r) => r.json()),
5000,
(data: { price: number }) => setPrice(data.price)
);
return <p>{symbol}:{price === null ? '加载中…' : `¥${price}`}</p>;
}
export function Dashboard() {
const [tab, setTab] = useState<'counter' | 'ticker' | 'notes'>('counter');
const [notes, setNotes] = useState('');
return (
<section>
<nav>
{(['counter', 'ticker', 'notes'] as const).map((t) => (
<button key={t} aria-pressed={tab === t} onClick={() => setTab(t)}>
{t === 'counter' ? '计时器' : t === 'ticker' ? '行情' : '笔记'}
</button>
))}
</nav>
{/* ⭐ 三个面板「始终挂载」,切换时只改可见性 */}
<Activity mode={tab === 'counter' ? 'visible' : 'hidden'}>
<LiveCounter label="计时器" />
</Activity>
<Activity mode={tab === 'ticker' ? 'visible' : 'hidden'}>
<StockTicker symbol="AAPL" />
</Activity>
<Activity mode={tab === 'notes' ? 'visible' : 'hidden'}>
<textarea value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="随手记…" />
</Activity>
{/* ⭐ 效果:
· 切到「行情」时:计时器【暂停】(秒数不涨),但「已运行 30 秒」的值保留着
· 切回「计时器」时:从 30 继续(不是 0),且定时器重新开始跑
· 在「笔记」里打字后切走再切回:内容还在(state 保留) */}
</section>
);
}
⭐⭐ 这个等价方案「做到了什么」与「做不到什么」(诚实说明):
✅ 做到了:
· 保留 state(因为组件没被卸载)
· 隐藏 DOM(hidden 属性 → 浏览器跳过布局与绘制)
· 暂停「遵循约定」的副作用(用 useActivityEffect 的那些)
❌ 做不到:
· 自动暂停「子组件里普通的 useEffect」
(用户代码无法拦截别人的 Hook —— 这是 React 内部才有的能力)
· 「预渲染一个从未显示过的子树」(需要 React 内部的离屏渲染)
· 「恢复时只重跑 effect 而不重跑渲染」(真正的 Activity 有更精细的调度)
⭐ 所以实践建议:
· 如果你的「不可见子树」的副作用可控 → 这个方案够用
· 如果需要「统一的、自动的」暂停语义 → 等 `<Activity>`(或用了 React 19.2+ 就直接用它)
五、与 Vue KeepAlive 的对比
| 维度 | Vue <KeepAlive> |
React <Activity> |
|---|---|---|
| 保留组件 state | ✅ | ✅ |
| 保留 DOM | ✅(默认缓存 VNode 与 DOM) | ✅(隐藏在 DOM 里) |
副作用(onMounted/onUnmounted) |
⚠️ 默认不重新触发(activated/deactivated 钩子提示) |
✅ 会清理并重建(useEffect 的清理/执行) |
| 预渲染 | ❌ | ✅(可预渲染「从未显示」的子树) |
| 与 Suspense 的关系 | 独立 API | 同一套底层机制(Suspense 内部就用它) |
| 缓存淘汰策略 | include/exclude/max |
由你控制(渲染哪个 Activity、mode 是什么) |
⭐ 一个关键的设计差异:
· Vue KeepAlive:组件「被缓存」——所以生命周期会「暂停」,靠 `activated`/`deactivated` 通知
· React Activity:组件「保持挂载但隐藏」——副作用会被【真正清理】,
所以「切换 = 断开 + 重连」,而不是「暂停 + 恢复」
⭐ 这带来两个实践差异:
① React 里「不可见时」的资源是「被释放」的(更省,但恢复有成本)
② Vue 里「不可见时」的定时器仍在跑(要在 deactivated 里手动停)
六、本篇特有的坑
// ① 用 Activity 包「一次性弹窗」→ state 与资源一直留着
<Activity mode={open ? 'visible' : 'hidden'}><ConfirmDialog /></Activity>
// ⚠️ 如果这个弹窗「不会再次打开」,纯属浪费内存
// ✅ 一次性内容用条件渲染
// ② 以为 Activity 能「自动暂停子组件的普通 useEffect」
// ⚠️ 用户代码做不到;需要子组件用 useActivityEffect(或等真正的 Activity)
// ③ 在 hidden 的子树里「依赖定时器驱动渲染」但用了普通 useEffect
useEffect(() => {
const id = setInterval(() => setN((v) => v + 1), 50); // ⚠️ 不可见时仍在跑,白白浪费
return () => clearInterval(id);
}, []);
// ✅ 用 useActivityEffect(或真正的 Activity)
// ④ 用 `display: contents` 包裹时「误以为它能当布局容器」
// ⚠️ display:contents 会让元素「不生成盒子」→ 不能设 padding/margin/背景
// ⑤ 在 Activity 子树里「用 IntersectionObserver 判断可见性」
// ⚠️ hidden 的元素「永远不会 intersect」→ 相关逻辑会被冻结(有时是期望的,有时不是)
// ⑥ hidden 期间「用户仍能通过键盘 Tab 到隐藏的控件」
// ✅ 真正不可交互要同时设 `inert` 或 `aria-hidden` + `tabindex="-1"`
// (React 19 支持 `inert` 属性:<div inert={!active}>)
// ⑦ 把 hidden 的子树当作「已卸载」来写(比如在 cleanup 里 setState)
// ⚠️ state 还在,但组件「看起来像被卸载了」——要区分「hidden」与「unmount」
// ⑧ 用 Activity 保留「大量列表」的 state → 内存暴涨
// ✅ 保留「状态轻、重建成本高」的子树;「数据重、状态轻」的用条件渲染
// ⑨ 在「服务端渲染」中假设 Activity 生效
// ⚠️ SSR 时不存在「隐藏/显示」的切换(那是客户端行为);服务端通常把它当 visible 处理
// ⑩ 用 Activity 做「路由缓存」时忘记「数据过期」
// ⚠️ state 被保留了,但「数据可能已经旧了」→ 重新可见时要考虑 revalidate
// ⑪ 把「hidden 的子树」用于「预渲染」时,忘记了它「也会执行 Effect」
// ⚠️ 预渲染会跑一次渲染,但(真正的 Activity)不会跑 Effect——
// 所以「预渲染的组件不能依赖 Effect 才能显示内容」
// ⑫ 在「React 18」里 import { Activity }(不存在)
// ✅ React 18 用本文的等价方案;React 19.2+ 才有官方组件
// ⑬ 认为「hidden 等于 display:none」(在某些实现里也可以用 hidden 属性或 visibility)
// ⚠️ 两者对「可访问性/焦点/动画」的影响不同:
// display:none → 不占位、不可聚焦、不参与布局
// visibility:hidden → 占位、不可聚焦
// hidden 属性 → 等价于 display:none(但可被 CSS 覆盖)
// ⑭ 在 hidden 子树里「播放视频/音频」并期待它停止
// ⚠️ display:none 不会自动暂停媒体(要自己监听可见性并 pause)
// ⑥ 的完整写法:真正「不可交互 + 不被读屏读到」
function ActivityAccessible({ mode, children }: { mode: 'visible' | 'hidden'; children: React.ReactNode }) {
const active = mode === 'visible';
return (
<div hidden={!active} inert={!active} aria-hidden={!active}>
{/* ⭐ inert:整个子树不可聚焦、不可交互(React 19 起可直接传 boolean)
aria-hidden:读屏软件跳过它 */}
{children}
</div>
);
}
// ⑩ 的完整处理:重新可见时「按需刷新数据」
function ProfilePanel() {
const active = useIsActive();
const { data, refetch, dataUpdatedAt } = useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 60_000,
});
useActivityEffect(() => {
// ⭐ 每次「重新可见」时检查数据是否过期,过期就刷新
if (Date.now() - dataUpdatedAt > 60_000) void refetch();
}, [active, dataUpdatedAt, refetch]);
return <>{data ? <ProfileView data={data} /> : <Skeleton />}</>;
}
七、面试延伸
- 「
Activity(Offscreen)解决了什么问题?」
它解决「隐藏、保留状态、不浪费资源」这个三方矛盾。传统的两种做法都不完整:① 条件渲染——组件被卸载,state 丢失(回来时要重新取数、重新挂载);② CSS 隐藏——state 与 DOM 都保留,但副作用继续跑(轮询还在发请求、WebSocket 还在收消息、定时器还在 setState、动画还在耗 CPU)。<Activity mode="hidden">的做法是「卸载副作用(执行所有清理函数)但保留 state 与 DOM」,因此既能保留用户上下文,又能真正停掉运行时资源。 -
「它和
Suspense是什么关系?」
Suspense内部就是用 Offscreen/Activity 实现的——当子树挂起时,React 需要「把已经渲染出来的部分隐藏起来而不是删掉」(等数据就绪后再显示),这正是「保留 DOM + 隐藏」的语义。所以可以说「Activity 是 Suspense 的底层能力被公开出来」。反过来,mode="hidden"的子树在「预渲染」时也会用到 Suspense 的离屏渲染机制。 -
「
mode="hidden"时到底发生了什么?」
四件事:① 执行子树里所有 Effect 的清理函数(订阅取消、定时器清除、请求 abort、Observer disconnect)——与「卸载」相同的清理流程;② 保留组件的 state(下次可见时useState的值还在,不需要重新取数);③ 保留 DOM,但设为不可见(因此浏览器会跳过布局与绘制);④ 对「从未显示过的子树」可以预渲染(在后台先渲染好,等可见时立刻可交互)。 -
「它和
KeepAlive(Vue)有什么区别?」
① 副作用的语义——Vue 的KeepAlive是「缓存」(生命周期暂停,靠activated/deactivated通知,定时器默认仍在跑);React 的Activity是「保持挂载但隐藏」(副作用被真正清理,切换等价于「断开 + 重连」)。② 能力范围——Activity还支持「预渲染从未显示过的子树」,并且与Suspense共用同一套底层机制。③ 淘汰策略——KeepAlive有include/exclude/max;Activity由你控制「渲染哪几个、mode 是什么」。 -
「什么时候该用它,什么时候不该?」
该用:① 「用户大概率会回来」的场景(Tab 切换、Modal 重开、路由后退);② 「不可见期间副作用有成本」的场景(轮询、WebSocket、定时器、动画)。不该用:① 「一次性内容」(弹窗走完就再也不打开)→ 条件渲染更省内存;② 「数据很重但状态很轻」的子树(保留 state 的收益不抵内存占用)→ 条件渲染;③ 纯静态、无副作用的展示 → CSS 隐藏就够(但 Activity 也不亏)。 -
「它有什么代价?」
三点:① 内存——被隐藏的子树「state 与 DOM 都还在」,如果保留很多大列表,内存会持续增长;② 恢复成本——mode从 hidden 变 visible 时,所有 Effect 会被重新执行(重新订阅、重新连接、重新请求),所以「恢复」不是零成本;③ 数据可能过期——state 被保留了,但数据可能是旧的,需要在「重新可见时」做 revalidate(例如检查staleTime)。所以「保留什么」需要按「重建成本 vs 内存占用」权衡。 -
「
display: none和hidden属性有什么区别?」
三个相关的隐藏方式:①display: none——元素不生成盒子,不占位、不可聚焦、不参与布局与绘制;②hidden属性——语义上等价于display: none(但可以被 CSS 覆盖,如[hidden] { display: block });③visibility: hidden——仍占位(参与布局)、不可见、不可聚焦。选择取决于「是否要占位」:不想占位用前两者;想占位(避免布局跳动)用visibility。另外「不可交互」还需要inert(React 19 可直接传 boolean),「读屏跳过」需要aria-hidden。 -
「为什么『用户代码』模拟不了真正的
Activity?」
因为真正的Activity需要 React 在内部做三件事:① 遍历子树并自动执行所有 Effect 的清理(用户代码无法拦截「别人写的useEffect」——Hook 的调度权在 React 手里);② 离屏预渲染(「渲染但不提交到屏幕」,这需要控制 commit 流程);③ 恢复时的精细调度(只重跑 effect、不重跑渲染)。用「Context + 自定义 Hook」的等价方案只能覆盖「遵循约定」的副作用,且无法预渲染。这也是「为什么它必须由 React 提供」的原因。
一句话速记
<Activity>(内部代号 Offscreen)在mode="hidden"时「执行所有 Effect 的清理函数,但保留 state 与 DOM」——补上了「条件渲染会丢 state」与「CSS 隐藏副作用继续跑」之间的空白;它是 Suspense 的底层机制公开化(Suspense 挂起时就是「隐藏已渲染部分」),并支持「预渲染从未显示过的子树」;适用「用户会回来 + 不可见时副作用有成本」的场景(Tab、Modal、路由后退、轮询/WebSocket 密集的页面);代价是内存占用、恢复时 Effect 重建、数据可能过期(需要 revalidate);与 VueKeepAlive的关键差异是「它真正清理副作用(切换=断开+重连),而 KeepAlive 是缓存(activated/deactivated,定时器默认仍在跑)」;用户代码无法完全模拟(拿不到 Hook 调度权),可用「Context +useActivityEffect」覆盖 90% 的场景;配合inert/aria-hidden才能让隐藏内容不可交互、不被读屏读到。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!