ReactDOM.createPortal 与 position: fixed 的本质区别

在前端开发中,我们常遇到这样的问题:

为什么我的 tooltip、弹窗、下拉菜单明明是 position: fixed,却还是被父元素裁剪、遮挡?

很多人以为是 overflow: hiddenz-index 的问题,但实际上------根本原因在于 "包含块 (containing block)" 的创建机制

1. overflow 本身不会影响 fixed

单独的 overflow: hidden 并不会让 fixed 元素失效:

css 复制代码
<div style={{ overflow: 'hidden' }}>
  <div style={{ position: 'fixed', top: 0 }}>我是 fixed</div>
</div>

上例中,fixed 仍然相对 视口 (viewport) 定位,不会被裁剪。

2. 谁在"偷走"你的 fixed?

当某个祖先元素具有以下任意属性时,它会创建一个新的 包含块 (containing block) ,从而让 fixed 元素失去对 viewport 的参照:

属性 示例
transform transform: translate(0, 0)
filter filter: blur(0)
perspective perspective: 1000px
contain contain: layout/paint
will-change will-change: transform
backdrop-filter backdrop-filter: blur(10px)

一旦出现这些属性,position: fixed 的元素将不再相对窗口定位,而是相对该祖先元素定位。

3. 被裁剪的真相

如果上述祖先元素同时还设置了 overflow: hiddenoverflow: auto

则 fixed 元素的可视区域也会被父元素裁剪掉:

less 复制代码
<div style={{
  transform: 'translateZ(0)',  // 关键:创建包含块
  overflow: 'hidden',
  border: '1px solid red',
  height: 100,
  width: 100,
}}>
  <div style={{
    position: 'fixed', 
    top: 0, 
    left: -20,
    background: 'black', 
    color: 'white'
  }}>
    我被裁剪了
  </div>
</div>

4. ReactDOM.createPortal 的作用

ReactDOM.createPortal 会把组件直接渲染到 document.body(或指定容器):

javascript 复制代码
ReactDOM.createPortal(
  <Tooltip />,
  document.body
)

这样做可以完全绕过那些"有问题的父容器",

确保你的 tooltip、弹窗永远相对 viewport 定位,不受裁剪或 transform 干扰。

5. Tooltip 对比示例

下面这段代码展示了同样的 tooltip,在 非 portalportal 模式下的差异。

less 复制代码
import React from 'react';
import ReactDOM from 'react-dom';

function NonPortalTooltip() {
  return (
    <div style={{
      position: 'relative',
      width: 200,
      height: 120,
      overflow: 'hidden',
      transform: 'translateZ(0)', // 创建包含块
      border: '1px solid #aaa',
      marginBottom: 20
    }}>
      <div style={{ padding: 8 }}>父容器(overflow + transform)</div>
      <div style={{
        position: 'fixed',
        top: 80, left: 150,
        background: 'black',
        color: 'white',
        padding: '4px 8px'
      }}>
        非 Portal Tooltip(会被裁剪)
      </div>
    </div>
  );
}

function PortalTooltip() {
  return (
    <div style={{
      position: 'relative',
      width: 200,
      height: 120,
      overflow: 'hidden',
      transform: 'translateZ(0)',
      border: '1px solid #aaa'
    }}>
      <div style={{ padding: 8 }}>父容器(overflow + transform)</div>
      {ReactDOM.createPortal(
        <div style={{
          position: 'fixed',
          top: 80, left: 150,
          background: 'green',
          color: 'white',
          padding: '4px 8px'
        }}>
          Portal Tooltip(不会被裁剪)
        </div>,
        document.body
      )}
    </div>
  );
}

export default function App() {
  return (
    <>
      <NonPortalTooltip />
      <PortalTooltip />
    </>
  );
}

运行后你会看到:

  • 黑色 tooltip(非 portal)会被父容器裁剪;
  • 绿色 tooltip(通过 portal 渲染)则始终完整可见。

✅ 一句话总结

position: fixed 相对视口定位的前提是:祖先元素没有创建新的包含块

ReactDOM.createPortal 的意义,就是让元素彻底脱离这些祖先的影响。

相关推荐
HuangYongbiao3 小时前
Rspack 插件架构原理:从 Tapable 到 Rust Hook
前端·架构
Cache技术分享3 小时前
231. Java 集合 - 将集合元素转换为数组
前端·后端
神秘的猪头3 小时前
浏览器是如何渲染 HTML/CSS/JS 页面的?——从代码到像素的完整流程
前端·javascript
啷咯哩咯啷3 小时前
el-table-v2 实现自适应列宽
前端·javascript·vue.js
jump6803 小时前
为什么typeof null = 'object'
前端
__不想说话__3 小时前
给网站做“体检”:Lighthouse如何平息产品经理的怒火
前端·google·架构
玉宇夕落3 小时前
🚀 从 HTML 到像素:浏览器渲染全流程揭秘(附性能优化实战)
前端·dom
西甲甲3 小时前
chromium UI 简要解析
前端
Holin_浩霖3 小时前
函数式编程实现简单的 Fiber 架构
前端