Portal 与覆盖层
Portal 与覆盖层 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完你能用 createPortal 把对话框渲染到 body 下的独立节点,避开父级 overflow 与层叠上下文的裁剪;会逐项检查覆盖层四件套——层叠、滚动锁定、焦点、关闭行为——并理解 portal 内事件仍沿 React 树冒泡这一关键性质。
问题场景
课程编辑页的侧栏套了 transform 和 overflow: hidden(折叠动画需要)。在里面弹「删除确认」对话框:对话框被侧栏裁掉一半;z-index 拉到 9999 也没用——父级的 transform 创建了新的层叠上下文,内部 z-index 再大也出不了这个圈子。界面要「浮」在一切之上,DOM 位置就必须换地方。
核心解释
Portal 的解法很直接:组件渲染到哪个 DOM 节点,和它在 React 树里的位置,是两回事:
function ConfirmDialog({ open, onClose }) {
useEffect(() => {
if (!open) return;
const onKey = (e) => { if (e.key === "Escape") onClose(); };
document.addEventListener("keydown", onKey);
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden"; // 滚动锁定
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prev; // 关闭时恢复
};
}, [open, onClose]);
if (!open) return null;
return ReactDOM.createPortal(
<div className="overlay" onClick={onClose}>
<div className="dialog" onClick={(e) => e.stopPropagation()}>…</div>
</div>,
document.body // 真实 DOM 挂到 body 下,跳出所有裁剪
);
}
焦点管理是四件套里最容易被漏的:打开后把焦点移进对话框,键盘用户才知道「浮层已打开」:
const cancelRef = useRef(null);
useEffect(() => {
if (open && cancelRef.current) cancelRef.current.focus();
}, [open]);
两个不直观但重要的性质:事件按 React 树冒泡,不按 DOM 树——对话框里的 onClick 在 React 父组件照样能收到(除非 stopPropagation);context 正常工作,portal 内读到的 context 由 React 树位置决定,与 DOM 位置无关。
覆盖层检查清单(改完必查):层叠——真实 DOM 在 body 末尾,天然盖住应用节点,不再打 z-index 战争;滚动锁定——打开锁 body、关闭恢复原值;焦点——打开即移入,Esc 可退;关闭行为——点遮罩关、Esc 关,内容区 stopPropagation 防误触。
示例拆解
实验区左侧是一个带 transform + overflow hidden 的「侧栏」,里面放「删除课程」按钮:
- 不用 portal:对话框渲染在侧栏里,被裁剪、被遮挡;
- 用 portal:同样的按钮,对话框完整出现在屏幕中央(渲染进 body);
- 打开后:页面滚动被锁定,首焦点落在按钮上,Esc 关闭并恢复滚动;
- 页面底部有事件日志区,记录 React 树上的冒泡情况;
- 验证:两种实现各开关一次,逐项检查裁剪、滚动、焦点、Esc 四项,再用 DevTools 的 Elements 面板确认 portal 节点真的在 body 下。
动手任务
打开本课实验区:
- 勾选「首焦点给确认」,打开对话框按 Tab,确认焦点从确认按钮开始移动。
- 在 React 根节点的日志逻辑里观察:点击对话框内部按钮后,事件沿 React 树冒泡到了根;再把内容区的 stopPropagation 临时去掉,对比日志差异。
- 给页面加一段很高的占位内容,打开对话框后尝试滚动页面,关闭后确认滚动恢复。
常见错误
- 以为 portal 会破坏事件冒泡:React 事件沿组件树冒泡,DOM 位置不影响;需要阻断时显式 stopPropagation。
- 忘了恢复 body 样式:关闭对话框后滚动一直锁死;清理函数里恢复原值。
- z-index 战术叠加:portal 已跳出层叠上下文,再叠 9999 只会让浮层关系更乱;按 DOM 顺序管理层级。
- 打开时不移焦点:键盘用户不知道浮层已打开;打开后主动 focus 到内部控件。
检查与复盘
自查清单:
- 能说出 portal 的核心:「DOM 位置」与「React 树位置」分离,事件与 context 沿 React 树
- 覆盖层四件套(层叠/滚动/焦点/关闭)逐项验证过
- 清理函数恢复了 body 与监听器,反复开关无残留
参考资料
- React 官方文档:createPortal —— API 语义与事件冒泡说明
- React 官方文档:逃逸通道 —— 什么时候「跳出 React」是合理的
读完这一课了?
标记完成后,工作台会安排接下来的学习。