React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React浮层组件

React实现浮层组件的思路与方法详解

作者:lecepin

React 浮层组件(也称为弹出组件或弹窗组件)通常是指在用户界面上浮动显示的组件,本文主要介绍了浮层组件的实现方法,感兴趣的小伙伴可以了解下

React 浮层组件(也称为弹出组件或弹窗组件)通常是指在用户界面上浮动显示的组件,它们脱离常规的文档流,并且可以在用户进行某些操作时出现在页面的最上层。React 浮层组件可以用于创建模态框(Modal)、下拉菜单(Dropdown)、工具提示(Tooltip)、侧边栏(Sidebar)或任何其他需要动态显示和隐藏且通常位置固定或绝对定位的内容。

React 浮层组件的特点包括:

1. 实现简单的 Tootip

Tootip 就是简单的文字提示气泡框。

简单实现:

import React, { useState, useRef } from "react";

const Tooltip = ({ children, text }) => {
  const [visible, setVisible] = useState(false);
  const tooltipRef = useRef();

  const showTooltip = () => setVisible(true);
  const hideTooltip = () => setVisible(false);

  // 获取触发元素的位置,以便正确定位Tooltip
  const computePosition = () => {
    if (tooltipRef.current) {
      const { top, height } = tooltipRef.current.getBoundingClientRect();
      return {
        top: top + height + window.scrollY,
        left: 0 + window.scrollX,
      };
    }

    return { top: 0, left: 0 };
  };

  const tooltipStyle = {
    position: "absolute",
    border: "1px solid #ccc",
    backgroundColor: "white",
    padding: "5px",
    zIndex: 1000,
    ...computePosition(), // 计算位置并应用样式
    display: visible ? "block" : "none", // 控制显示与隐藏
  };

  return (
    <div
      style={{ position: "relative", display: "inline-block" }}
      ref={tooltipRef}
    >
      <div
        style={tooltipStyle}
        onMouseEnter={showTooltip}
        onMouseLeave={hideTooltip}
      >
        {text}
      </div>
      <div onMouseEnter={showTooltip} onMouseLeave={hideTooltip}>
        {children}
      </div>
    </div>
  );
};

// 使用Tooltip的组件
const App = () => {
  return (
    <div>
      <p>
        Hover over the word{" "}
        <Tooltip text="A helpful tooltip.">"tooltip"</Tooltip> to see the
        tooltip.
      </p>
    </div>
  );
};

2. 存在问题

上面的 Tooltip 实现确实可能带来一些问题。以下是一些主要考虑点:

影响原布局

可能被隐藏

性能问题

可访问性问题

屏幕适配问题

复杂度增加

当页面中有许多 Tooltip 时,管理它们的位置和可见性状态可能会变得复杂。

为了解决这些问题,您可能需要进一步优化 Tooltip 组件,例如:

尽管有这些潜在的问题,但在某些情况下,这种不使用 createPortal 的实现方式可能足够满足简单的需求。对于更复杂的情况,则可能需要考虑更高级的解决方案,例如使用 React.createPortal 或第三方库,这些库已经解决了上述问题。

3. createPortal

ReactDOM.createPortal 是 React 提供的一个 API,它允许你把子节点渲染到存在于父组件之外的 DOM 节点上。这个方法的签名如下:

ReactDOM.createPortal(child, container);

其中 child 是任何可以渲染的 React 子元素,例如一个元素、字符串或碎片(fragment),而 container 是一个 DOM 元素。

createPortal 的主要用处包括:

事件冒泡:使用 createPortal 渲染的子组件会在 DOM 树上处于不同的位置,但从 React 的角度来看,它仍然存在于 React 组件树中原来的位置,这意味着事件可以正常冒泡到 React 父组件,尽管这些元素在 DOM 层级结构中并不直接相连。

避免 CSS 约束:在某些情况下,你可能不希望子组件受到父组件 CSS 的影响,例如在一个设置了overflow: hiddenz-index的父元素内部渲染一个模态对话框(modal)或工具提示(tooltip)。通过使用 createPortal,模态或工具提示可以渲染到 DOM 树中的其他位置,例如直接渲染到document.body下,从而避免了这些 CSS 约束。

视觉上的“脱离”:当你需要组件在视觉上位于页面层次结构之外时(如模态框、通知、悬浮卡片等),createPortal 提供了一种将组件结构上与视觉结构分离的方法。这可能是因为这些组件需要在页面上占据最顶层,以避免被其他元素遮挡。

createPortal 注意事项

假设你指的是 "reactportal" 中可能遇到的问题,那么在使用 React 的 createPortal API 时,以下是一些可能遇到的典型问题和挑战:

为了解决这些问题,你可能需要实施一些策略,比如在样式上使用更高的特异性,使用辅助技术友好的方法来管理焦点,或者确保上下文在 Portal 内部和外部保持一致。此外,对于性能和兼容性问题,可能需要一些额外的优化和测试。

4. cloneElement

React 的 cloneElement 函数允许你克隆一个 React 元素,并传入新的 props、ref。这个函数的签名如下:

React.cloneElement(element, [props], [...children]);

cloneElement 主要用于以下几种场景:

以下是一个使用 cloneElement 来增强子组件 onMouseEnter 事件的例子:

import React, { cloneElement } from "react";

class EnhancedComponent extends React.Component {
  handleMouseEnter = () => {
    // 提供额外的 onMouseEnter 行为
    console.log("Mouse entered!");
  };

  render() {
    const { children } = this.props;

    // 假设我们只处理一个子元素的情况
    const child = React.Children.only(children);

    // 克隆子元素并注入新的 onMouseEnter 处理器
    const enhancedChild = cloneElement(child, {
      onMouseEnter: this.handleMouseEnter,
    });

    return enhancedChild;
  }
}

export default EnhancedComponent;

在这个例子中,EnhancedComponent 接收一个单一的子组件,并使用 cloneElement 对其进行克隆,添加一个新的 onMouseEnter 事件处理器。如果原始子组件已经有 onMouseEnter 处理器,这个新的处理器会被合并,两个处理器都会被执行。

总之,cloneElement 在不同的用例中都很有用,尤其是当你想要微调元素的属性或行为而又不想创建全新的组件时。然而,过度使用 cloneElement 可能使组件变得难以理解和维护,因此应该谨慎使用。

5. 重写 Tootip

使用 cloneElement 和 createPortal 来实现一个 Tooltip 组件,可以将 Tooltip 的内容渲染到页面的顶级位置,同时注入事件处理器到触发元素。这样的实现可以解决上文提到的一些问题,例如避免因为 overflow 或 z-index 造成的渲染问题。

以下是一个简单的函数式组件模式的 Tooltip 实现:

import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";

const Tooltip = ({ children, content }) => {
  const [show, setShow] = useState(false);
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const childRef = useRef(null);

  const handleMouseEnter = () => {
    if (childRef.current) {
      const rect = childRef.current.getBoundingClientRect();
      setPosition({
        top: rect.bottom + window.scrollY,
        left: rect.left + window.scrollX,
      });
    }
    setShow(true);
  };

  const handleMouseLeave = () => {
    setShow(false);
  };

  useEffect(() => {
    window.addEventListener("scroll", handleMouseLeave);
    return () => {
      window.removeEventListener("scroll", handleMouseLeave);
    };
  }, []);

  const tooltip =
    show &&
    ReactDOM.createPortal(
      <div
        style={{
          position: "absolute",
          top: position.top,
          left: position.left,
          zIndex: 1000,
          backgroundColor: "#fff",
          border: "1px solid #ddd",
          padding: "5px",
          borderRadius: "3px",
          boxShadow: "0 2px 5px rgba(0,0,0,0.2)",
        }}
      >
        {content}
      </div>,
      document.body
    );

  const clonedChild = React.cloneElement(children, {
    onMouseEnter: handleMouseEnter,
    onMouseLeave: handleMouseLeave,
    ref: childRef,
  });

  return (
    <>
      {clonedChild}
      {tooltip}
    </>
  );
};

// 使用Tooltip的组件
const App = () => {
  return (
    <div style={{ marginTop: "100px", marginLeft: "100px" }}>
      <Tooltip content="This is a tooltip!">
        <button>Hover over me!</button>
      </Tooltip>
    </div>
  );
};

export default App;

在这个例子中,Tooltip 组件接受一个 children 属性和一个 content 属性。children 是触发 Tooltip 的元素,content 是显示在 Tooltip 中的内容。

使用 cloneElement,我们为 children 元素克隆一个新版本并添加了 onMouseEnter 和 onMouseLeave 事件处理器,用于控制 Tooltip 的显示和隐藏。

我们使用 createPortal 将 Tooltip 内容渲染到 document.body 中,这样 Tooltip 就能够避开任何本地 CSS 的限制。show 状态控制 Tooltip 的显示,position 状态用于计算 Tooltip 应该出现在页面上的位置。

通过 useEffect,我们添加了对 scroll 事件的监听来在页面滚动时隐藏 Tooltip,防止 Tooltip 位置不正确。

这样,你就得到了一个使用 cloneElement 和 createPortal 实现的 Tooltip 组件,它可以在不影响页面布局和样式的情况下工作,并且能够在页面上的任何位置正确地显示 Tooltip。

5.1 同步元素滚动

上面的代码示例中,Tooltip 在触发元素的 onMouseEnter 事件处理器中计算其显示位置,并在 onMouseLeave 或窗口的 scroll 事件中被隐藏。这意味着一旦 Tooltip 显示出来,如果用户滚动页面,Tooltip 会保持在首次显示时计算出的固定位置,而不会跟随触发元素移动。

如果要实现 Tooltip 位置与触发元素同步滚动的效果,需要动态更新 Tooltip 的位置,以响应页面的滚动事件。这可以通过在 useEffect 钩子中添加对滚动事件的监听来实现。在滚动事件的回调中,我们可以重新计算 Tooltip 的位置,使其与触发元素保持同步。

以下是更新后的代码示例,实现了 Tooltip 与触发元素同步滚动的效果:

import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";

const Tooltip = ({ children, content }) => {
  const [show, setShow] = useState(false);
  const childRef = useRef(null);

  const updatePosition = () => {
    if (childRef.current) {
      const rect = childRef.current.getBoundingClientRect();
      return {
        top: rect.bottom + window.scrollY,
        left: rect.left + window.scrollX,
      };
    }
  };

  const [position, setPosition] = useState({ top: 0, left: 0 });

  const handleMouseEnter = () => {
    setPosition(updatePosition());
    setShow(true);
  };

  const handleMouseLeave = () => {
    setShow(false);
  };

  useEffect(() => {
    const handleScroll = () => {
      if (show) {
        setPosition(updatePosition());
      }
    };

    window.addEventListener("scroll", handleScroll);

    // 清理函数
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [show]); // 依赖于 `show`,仅当 Tooltip 显示时添加事件监听

  const tooltip =
    show &&
    ReactDOM.createPortal(
      <div
        style={{
          position: "absolute",
          top: position.top,
          left: position.left,
          zIndex: 1000,
          backgroundColor: "#fff",
          border: "1px solid #ddd",
          padding: "5px",
          borderRadius: "3px",
          boxShadow: "0 2px 5px rgba(0,0,0,0.2)",
          // 添加 transition 效果使位置更新更平滑
          transition: "top 0.3s, left 0.3s",
        }}
      >
        {content}
      </div>,
      document.body
    );

  const clonedChild = React.cloneElement(children, {
    onMouseEnter: handleMouseEnter,
    onMouseLeave: handleMouseLeave,
    ref: childRef,
  });

  return (
    <>
      {clonedChild}
      {tooltip}
    </>
  );
};

// 使用Tooltip的组件
const App = () => {
  return (
    <div style={{ marginTop: "100px", marginLeft: "100px" }}>
      <Tooltip content="This is a tooltip!">
        <button>Hover over me!</button>
      </Tooltip>
    </div>
  );
};

export default App;

在这个更新的实现中,添加了 updatePosition 函数,它负责根据当前触发元素的位置来更新 Tooltip 的位置。在 useEffect 中注册了页面滚动的事件监听器 handleScroll,它会在页面滚动时调用 updatePosition 来更新 Tooltip 的位置。监听器仅在 Tooltip 显示时进行注册,从而避免不必要的事件监听和执行。

综上所述,更新后的代码使得 Tooltip 能够在页面滚动时跟随触发元素移动,从而解决了位置同步的问题。

5.2 防止 cloneElement 注入破坏

在使用 cloneElement 对子组件增强或注入新的属性和事件处理器时,需要特别注意不要覆盖子组件原有的属性和事件处理器。为了避免这种覆盖,可将原有的属性和处理器与新的属性和处理器合并。

下面是一个示例,它展示了如何使用 cloneElement 来增强子组件的 onMouseEnter 和 onMouseLeave 事件处理器,同时保留原有的事件处理器:

import React, { useState, useRef } from "react";
import ReactDOM from "react-dom";

const Tooltip = ({ children, content }) => {
  const [show, setShow] = useState(false);
  const childRef = useRef(null);

  const handleMouseEnter = (originalOnMouseEnter) => (event) => {
    // 如果子组件有自己的 onMouseEnter 事件处理器,先调用它
    if (originalOnMouseEnter) {
      originalOnMouseEnter(event);
    }
    // 然后执行 Tooltip 特定的逻辑
    setShow(true);
  };

  const handleMouseLeave = (originalOnMouseLeave) => (event) => {
    // 如果子组件有自己的 onMouseLeave 事件处理器,先调用它
    if (originalOnMouseLeave) {
      originalOnMouseLeave(event);
    }
    // 然后执行 Tooltip 特定的逻辑
    setShow(false);
  };

  const tooltipElement =
    show &&
    ReactDOM.createPortal(
      <div
        style={
          {
            /* Tooltip样式 */
          }
        }
      >
        {content}
      </div>,
      document.body
    );

  // 克隆子组件,合并事件处理器
  const clonedChild = React.cloneElement(children, {
    ref: childRef,
    onMouseEnter: handleMouseEnter(children.props.onMouseEnter),
    onMouseLeave: handleMouseLeave(children.props.onMouseLeave),
  });

  return (
    <>
      {clonedChild}
      {tooltipElement}
    </>
  );
};

// 使用Tooltip的组件
const App = () => {
  const handleButtonMouseEnter = () => {
    console.log("Button's original onMouseEnter called");
  };

  const handleButtonMouseLeave = () => {
    console.log("Button's original onMouseLeave called");
  };

  return (
    <div style={{ marginTop: "100px", marginLeft: "100px" }}>
      <Tooltip content="This is a tooltip!">
        <button
          onMouseEnter={handleButtonMouseEnter}
          onMouseLeave={handleButtonMouseLeave}
        >
          Hover over me!
        </button>
      </Tooltip>
    </div>
  );
};

export default App;

在 Tooltip 组件中,我们为 handleMouseEnter 和 handleMouseLeave 方法分别传入了子组件原有的 onMouseEnter 和 onMouseLeave 事件处理器。然后在这些方法的闭包中,如果存在原有的事件处理器,我们先调用这些原有的事件处理器,接着执行 Tooltip 的逻辑。这样一来,我们就可以确保子组件的原有行为不会被 Tooltip 组件的行为覆盖。

如此,cloneElement 能够安全地用于增强子组件,而不会破坏子组件的预期行为。

6. 其他优化

在实现 React 组件时,尤其是在开发像 Tooltip 这样可能大量使用的组件时,考虑性能优化是非常重要的。下面是一些性能优化的建议:

避免不必要的重新渲染

减少重计算

优化事件处理器

使用 shouldComponentUpdate 或 React.PureComponent:

减少 DOM 操作

对于使用 createPortal 的组件,尽可能减少 DOM 节点的插入和移除操作。可以考虑在应用的顶层预先定义好挂载点,而不是动态创建和销毁。

利用 CSS 动画代替 JS 动画

当可能时,使用 CSS 动画和过渡效果,因为它们可以利用 GPU 加速,而 JS 动画可能会触发更多的重绘和回流。

使用懒加载

如果 Tooltip 内容很大或包含图片等资源,可以考虑使用懒加载技术,只有当 Tooltip 显示时才加载内容。

避免内联函数定义

避免在渲染方法中定义内联函数,因为这将在每次渲染时创建新的函数实例,这可能会导致子组件的不必要重渲染。

分离组件

将大型组件拆分成更小、更容易管理的子组件,这样可以更精细地控制渲染行为。

使用 key 属性

当渲染列表或集合时,确保每个元素都有一个独特的 key 属性,这可以帮助 React 在更新过程中识别和重用 DOM 节点。

通过实施上述优化策略,你可以提高组件的性能,特别是在渲染大量 Tooltip 或在滚动等高频事件触发时。性能优化是一个持续的过程,始终需要根据实际场景和应用需求来评估和调整。

以上就是React实现浮层组件的思路与方法详解的详细内容,更多关于React浮层组件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文