第 28 章:Activity 与 Offscreen——隐藏而不卸载
场景:一个购物车,切到别的 tab 再切回来,滚动位置、已勾选项最好还在。普通条件渲染 {active && <Cart/>} 切走时会把 Cart 整个卸载,回来全重来。React 19.2 稳定的 <Activity>(内部一直叫 Offscreen)就是干这个的:把子树藏起来,但不销毁。
先记住这一句:Activity 让子树不可见但保留:DOM 用
display:none隐藏,fiber 树和 state 原样保留,回来秒恢复。它用低优先的 OffscreenLane 渲染,不抢主渲染的时间片。
一图流:卸载 vs 隐藏
Activity 内部是怎么做的
updateActivityComponent(ReactFiberBeginWork.js:1121)按 props.mode 分两条路:
mode='visible':正常渲染,像个普通容器。mode='hidden':子树照常走beginWork/completeWork(状态、hooks 全部照算),但完整过程会标记成"隐藏",commit 阶段用display:none把它藏起来,而不是从 DOM 里删掉。
关键区别在 commit:卸载会跑删除逻辑(Deletion + 卸载 effect),隐藏只改样式。所以 useEffect 不会因为"切走"被清理,useState 也不会重置。
低优先的 OffscreenLane
隐藏子树用的是最高位的 OffscreenLane(ReactFiberLane.js:108)。这有两个含义:
- 它永远排在最后算,绝不抢正常更新的时间片(第 22 章的"最低优先车道")。
- 隐藏树里的更新(比如后台轮询改 state)走这个 lane,不影响页面主渲染。
第 13 章提过 update 上有个 OffscreenLane 位,就是这里用的:区分"隐藏树里的更新"和"本来就有的更新"。
19.2 的改名:Offscreen → Activity
这个 API 内部叫 OffscreenComponent 多年(updateOffscreenComponent,613 还在),19.2 作为稳定版发布时对外叫 <Activity>。名字换过,机制没换:还是"隐藏子树、保留状态、低优先渲染"。注意它有别于 Suspense:Suspense 是"等数据",Activity 是"藏起来但别动"。
动手实验
- 看隐藏不卸载:
<Activity mode={active ? 'visible' : 'hidden'}><Cart/></Activity>,切走时在updateActivityComponent(1121)打断点,再在 Cart 的useEffectcleanup 打断点,确认隐藏时 cleanup 没跑(没卸载)。 - 看 display:none:切走后查 DOM,Cart 的根节点还在,只是
display:none。 - 看 OffscreenLane:在隐藏树里触发一次 setState,观察它是不是以最低优先的 lane 排队。
小结
- Activity = 隐藏子树但保留状态,DOM 藏、fiber 树不删。
updateActivityComponent(1121)按mode走 visible / hidden 两路。- **OffscreenLane(108)**最低优先,隐藏树不抢主渲染。
- 19.2 从 Offscreen 改名 Activity,机制没变,别和 Suspense 搞混。
- 下一步:从 18 到 19 并发渲染的演进,第 29 章。