react - createPortal魔法传送门

文章目录

    • [一、什么是 Portal?](#一、什么是 Portal?)
    • [二、为什么需要 Portal?](#二、为什么需要 Portal?)
      • [1. 解决 CSS 层叠上下文问题](#1. 解决 CSS 层叠上下文问题)
      • [2. 处理全局性 UI 元素](#2. 处理全局性 UI 元素)
      • [3. 保持 DOM 结构合理性](#3. 保持 DOM 结构合理性)
    • [三、核心 API:createPortal](#三、核心 API:createPortal)
    • [四、Portal 的关键特性](#四、Portal 的关键特性)
      • [1. 事件冒泡机制](#1. 事件冒泡机制)
      • [2. 生命周期与上下文](#2. 生命周期与上下文)
    • 五、实战应用场景
      • [1. 模态对话框实现](#1. 模态对话框实现)
      • [2. 全局通知系统](#2. 全局通知系统)
    • 六、性能优化与最佳实践
      • [1. 复用 DOM 节点](#1. 复用 DOM 节点)
      • [2. 避免内存泄漏](#2. 避免内存泄漏)
      • [3. SSR 兼容性处理](#3. SSR 兼容性处理)
    • 七、特别注意

一、什么是 Portal?

Portal 是 React 提供的一种将子节点渲染到父组件 DOM 层次结构之外的 DOM 节点中的方法。就像魔法世界的"传送门",它允许我们将组件渲染到 DOM 树的任意位置,同时保持其在 React 树中的逻辑位置。

注意 :

这是一个 API,不是组件,他的作用是:将一个组件渲染到 DOM 的任意位置,跟 Vue 的 Teleport 组件类似。

js 复制代码
import { createPortal } from "react-dom";

function Modal() {
  return createPortal(
    <div className="modal">
      <h2>我是模态框</h2>
      <p>虽然我在这里定义,但我会出现在body末尾!</p>
    </div>
    document.body
  );
}

二、为什么需要 Portal?

1. 解决 CSS 层叠上下文问题

1,当父组件有 overflow: hiddenz-index 时,子组件可能被意外裁剪或遮盖。

2,解决position: fixed存在的一些问题。

2. 处理全局性 UI 元素

模态框、通知、工具、下拉框、全局 loading、提示、等需要突破容器限制。

3. 保持 DOM 结构合理性

将工具提示渲染到触发元素附近可能导致 DOM 结构混乱。

三、核心 API:createPortal

基本语法

js 复制代码
createPortal(children, domNode, key?)
  • children:任何可渲染的 React 子元素
  • domNode:已经存在的 DOM 节点
  • key(可选):用作 portal 的 key

完整示例

js 复制代码
function Tooltip({ children, targetId }) {
  const [isVisible, setIsVisible] = useState(false);
  const targetElement = document.getElementById(targetId);

  if (!targetElement) return null;

  return (
    <>
      {createPortal(
        isVisible && (
          <div className="tooltip">
            {children}
          </div>
        ),
        targetElement
      )}
    </>
  );
}

// 使用
<Tooltip targetId="btn-1">这是一个提示</Tooltip>
<button id="btn-1">悬停我</button>

四、Portal 的关键特性

1. 事件冒泡机制

虽然 DOM 结构不同,但事件仍然按照 React 树的结构冒泡

js 复制代码
function Parent() {
  const handleClick = () => {
    console.log("点击事件从Portal冒泡上来了!");
  };

  return (
    <div onClick={handleClick}>
      <p>父组件</p>
      <Modal /> {/* 使用createPortal渲染到body */}
    </div>
  );
}

2. 生命周期与上下文

Portal 组件完全保留 React 上下文和生命周期

js 复制代码
const ThemeContext = createContext("light");

function ThemedModal() {
  const theme = useContext(ThemeContext);

  useEffect(() => {
    console.log("Modal mounted");
    return () => console.log("Modal unmounted");
  }, []);

  return createPortal(<div className={`modal ${theme}`}>...</div>, document.body);
}

五、实战应用场景

1. 模态对话框实现

js 复制代码
function Modal({ children, onClose }) {
  const modalRoot = useMemo(() => document.createElement("div"), []);

  useEffect(() => {
    document.body.appendChild(modalRoot);
    return () => document.body.removeChild(modalRoot);
  }, [modalRoot]);

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    modalRoot
  );
}

2. 全局通知系统

js 复制代码
const notificationRoot = document.getElementById("notifications");

function Notification({ message }) {
  return createPortal(<div className="notification">{message}</div>, notificationRoot);
}

六、性能优化与最佳实践

1. 复用 DOM 节点

js 复制代码
const portalRoot = document.getElementById("portal-root");

function MyPortal({ children }) {
  // 使用useMemo避免重复创建节点
  const container = useMemo(() => document.createElement("div"), []);

  useEffect(() => {
    portalRoot.appendChild(container);
    return () => portalRoot.removeChild(container);
  }, [container]);

  return createPortal(children, container);
}

2. 避免内存泄漏

确保在组件卸载时清理 Portal 节点

js 复制代码
useEffect(() => {
  const div = document.createElement("div");
  document.body.appendChild(div);

  return () => {
    document.body.removeChild(div);
  };
}, []);

3. SSR 兼容性处理

js 复制代码
function SafePortal({ children }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return createPortal(children, document.body);
}

七、特别注意

推荐使用 createPortal 因为他更灵活,可以挂载到任意位置,而position: fixed,会有很多问题,在默认的情况下他是根据浏览器视口进行定位的,但是如果父级设置了transform、perspective、filter 或 backdrop-filter 属性非 none 时,他就会相对于父级进行定位,这样就会导致 Modal 组件定位不准确(他不是一定按照浏览器视口进行定位),所以不推荐使用。

相关推荐
前端小L2 小时前
双指针专题(五):灵活的起跳——「无重复字符的最长子串」
javascript·算法·双指针与滑动窗口
靓仔建2 小时前
在Electron用npm install 失败。
javascript·electron·npm
沛沛rh453 小时前
React 学习笔记:State、hook —— 组件的记忆
前端·javascript·react.js
前端小L11 小时前
双指针专题(三):去重的艺术——「三数之和」
javascript·算法·双指针与滑动窗口
web小白成长日记11 小时前
在Vue样式中使用JavaScript 变量(CSS 变量注入)
前端·javascript·css·vue.js
C_心欲无痕11 小时前
react - useImperativeHandle让子组件“暴露方法”给父组件调用
前端·javascript·react.js
霖鸣12 小时前
Minecraft通过kubejs进行简单魔改
javascript
JackieDYH12 小时前
HTML+CSS+JavaScript实现图像对比滑块demo
javascript·css·html
全栈前端老曹13 小时前
【前端路由】Vue Router 嵌套路由 - 配置父子级路由、命名视图、动态路径匹配
前端·javascript·vue.js·node.js·ecmascript·vue-router·前端路由