跳到主要内容

第 28 章:Activity 与 Offscreen——隐藏而不卸载

场景:一个购物车,切到别的 tab 再切回来,滚动位置、已勾选项最好还在。普通条件渲染 {active && <Cart/>} 切走时会把 Cart 整个卸载,回来全重来。React 19.2 稳定的 <Activity>(内部一直叫 Offscreen)就是干这个的:把子树藏起来,但不销毁

先记住这一句:Activity 让子树不可见但保留:DOM 用 display:none 隐藏,fiber 树和 state 原样保留,回来秒恢复。它用低优先的 OffscreenLane 渲染,不抢主渲染的时间片。

一图流:卸载 vs 隐藏

Activity 内部是怎么做的

updateActivityComponentReactFiberBeginWork.js:1121)按 props.mode 分两条路:

  • mode='visible':正常渲染,像个普通容器。
  • mode='hidden':子树照常走 beginWork/completeWork(状态、hooks 全部照算),但完整过程会标记成"隐藏",commit 阶段用 display:none 把它藏起来,而不是从 DOM 里删掉。

关键区别在 commit:卸载会跑删除逻辑(Deletion + 卸载 effect),隐藏只改样式。所以 useEffect 不会因为"切走"被清理,useState 也不会重置。

低优先的 OffscreenLane

隐藏子树用的是最高位的 OffscreenLaneReactFiberLane.js:108)。这有两个含义:

  1. 永远排在最后算,绝不抢正常更新的时间片(第 22 章的"最低优先车道")。
  2. 隐藏树里的更新(比如后台轮询改 state)走这个 lane,不影响页面主渲染。

第 13 章提过 update 上有个 OffscreenLane 位,就是这里用的:区分"隐藏树里的更新"和"本来就有的更新"。

19.2 的改名:Offscreen → Activity

这个 API 内部叫 OffscreenComponent 多年(updateOffscreenComponent613 还在),19.2 作为稳定版发布时对外叫 <Activity>。名字换过,机制没换:还是"隐藏子树、保留状态、低优先渲染"。注意它有别于 Suspense:Suspense 是"等数据",Activity 是"藏起来但别动"。

动手实验

  1. 看隐藏不卸载<Activity mode={active ? 'visible' : 'hidden'}><Cart/></Activity>,切走时在 updateActivityComponent(1121)打断点,再在 Cart 的 useEffect cleanup 打断点,确认隐藏时 cleanup 没跑(没卸载)。
  2. 看 display:none:切走后查 DOM,Cart 的根节点还在,只是 display:none
  3. 看 OffscreenLane:在隐藏树里触发一次 setState,观察它是不是以最低优先的 lane 排队。

小结

  • Activity = 隐藏子树但保留状态,DOM 藏、fiber 树不删。
  • updateActivityComponent(1121)按 mode 走 visible / hidden 两路。
  • **OffscreenLane(108)**最低优先,隐藏树不抢主渲染。
  • 19.2 从 Offscreen 改名 Activity,机制没变,别和 Suspense 搞混。
  • 下一步:从 18 到 19 并发渲染的演进,第 29 章。