React 中hooks之useLayoutEffect 用法总结以及与useEffect的区别

发布于:2025-02-10 ⋅ 阅读:(63) ⋅ 点赞:(0)

React useLayoutEffect

1. useLayoutEffect 基本概念

useLayoutEffect 是 React 的一个 Hook,它的函数签名与 useEffect 完全相同,但它会在所有的 DOM 变更之后同步调用 effect。它可以用来读取 DOM 布局并同步触发重渲染。

2. useLayoutEffect vs useEffect

2.1 执行时机对比

Hook 名称 执行时机 执行方式 使用场景
useEffect DOM 更新后且浏览器重新绘制屏幕之后异步执行 (组件渲染完成后) 异步执行,不阻塞浏览器渲染 大多数副作用,如数据获取、订阅
useLayoutEffect DOM 更新后且浏览器重新绘制屏幕之前同步执行(组件将要渲染时) 同步执行,会阻塞浏览器渲染 需要同步测量 DOM 或更新布局

2.2 执行顺序示例

function ExampleComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('useEffect 执行'); // 后执行
  });

  useLayoutEffect(() => {
    console.log('useLayoutEffect 执行'); // 先执行
  });

  return (
    <div onClick={() => setCount(c => c + 1)}>
      点击次数:{count}
    </div>
  );
}

3. useLayoutEffect 使用场景

3.1 DOM 测量和更新

function AutoHeight() {
  const [height, setHeight] = useState(0);
  const elementRef = useRef();

  useLayoutEffect(() => {
    // 在 DOM 更新后立即测量高度
    const element = elementRef.current;
    const elementHeight = element.getBoundingClientRect().height;
    
    if (elementHeight !== height) {
      // 立即更新高度,避免闪烁
      setHeight(elementHeight);
    }
  }, [height]);

  return (
    <div>
      <div ref={elementRef} style={{ height: height || 'auto' }}>
        内容
      </div>
      <div>当前高度: {height}px</div>
    </div>
  );
}

3.2 防止闪烁的工具提示

function Tooltip({ text, position }) {
  const tooltipRef = useRef();
  const [tooltipPosition, setTooltipPosition] = useState(position);

  useLayoutEffect(() => {
    const tooltip = tooltipRef.current;
    const rect = tooltip.getBoundingClientRect();
    
    // 检查是否超出视口
    if (rect.right > window.innerWidth) {
      // 立即调整位置,避免闪烁
      setTooltipPosition({
        ...position,
        left: position.left - (rect.right - window.innerWidth)
      });
    }
  }, [position]);

  return (
    <div
      ref={tooltipRef}
      style={{
        position: 'absolute',
        ...tooltipPosition
      }}
    >
      {text}
    </div>
  );
}

3.3 动画处理

function AnimatedComponent() {
  const elementRef = useRef();
  const [isVisible, setIsVisible] = useState(false);

  useLayoutEffect(() => {
    if (isVisible) {
      const element = elementRef.current;
      // 立即设置初始状态
      element.style.opacity = '0';
      element.style.transform = 'translateY(20px)';
      
      // 强制重排
      element.getBoundingClientRect();
      
      // 应用动画
      element.style.transition = 'opacity 0.5s, transform 0.5s';
      element.style.opacity = '1';
      element.style.transform = 'translateY(0)';
    }
  }, [isVisible]);

  return (
    <div>
      <button onClick={() => setIsVisible(true)}>显示</button>
      <div ref={elementRef}>
        动画内容
      </div>
    </div>
  );
}

3.4 滚动位置同步

function ScrollSync({ content }) {
  const scrollContainerRef = useRef();

  useLayoutEffect(() => {
    const element = scrollContainerRef.current;
    // 内容更新后立即滚动到底部
    element.scrollTop = element.scrollHeight;
  }, [content]); // 当内容更新时执行

  return (
    <div 
      ref={scrollContainerRef}
      style={{ 
        height: '200px', 
        overflow: 'auto' 
      }}
    >
      {content.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
}

3.5 Modal 定位

function Modal({ isOpen, children }) {
  const modalRef = useRef();

  useLayoutEffect(() => {
    if (isOpen) {
      const modalElement = modalRef.current;
      const viewportHeight = window.innerHeight;
      const modalHeight = modalElement.getBoundingClientRect().height;
      
      // 立即计算并设置最佳位置
      modalElement.style.top = \`\${Math.max(
        0,
        (viewportHeight - modalHeight) / 2
      )}px\`;
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div ref={modalRef} className="modal">
        {children}
      </div>
    </div>
  );
}

4. 性能考虑

4.1 何时使用 useLayoutEffect

  • 需要同步测量 DOM 元素
  • 需要在视觉更新前进行 DOM 修改
  • 需要避免闪烁或布局抖动
  • 处理依赖于 DOM 布局的动画

4.2 何时使用 useEffect

  • 数据获取
  • 订阅事件
  • 日志记录
  • 其他不需要同步 DOM 测量或修改的副作用

5. 最佳实践

  1. 优先使用 useEffect
// ✅ 大多数情况下使用 useEffect 即可
useEffect(() => {
  // 异步操作,不影响渲染
  fetchData();
}, []);
  1. 仅在必要时使用 useLayoutEffect
// ✅ 需要同步 DOM 测量和更新时使用 useLayoutEffect
useLayoutEffect(() => {
  // 同步操作,立即更新 DOM
  updateDOMPosition();
}, []);
  1. 注意性能影响
// ❌ 避免在 useLayoutEffect 中进行耗时操作
useLayoutEffect(() => {
  // 不要在这里进行大量计算或 API 调用
  heavyComputation();
}, []);

// ✅ 耗时操作应该放在 useEffect 中
useEffect(() => {
  heavyComputation();
}, []);
  1. 合理使用依赖项
function OptimizedComponent({ data }) {
  useLayoutEffect(() => {
    // 只在真正需要同步更新的依赖项发生变化时执行
  }, [data.layout]); // 只依赖布局相关的属性
}

6. 注意事项

  1. useLayoutEffect 在服务器端渲染(SSR)中会收到警告,因为它只能在客户端执行
  2. 过度使用 useLayoutEffect 可能会导致性能问题
  3. 应该将耗时的操作放在 useEffect 中,只在 useLayoutEffect 中处理视觉相关的同步更新
  4. 在条件语句中使用时需要注意 Hook 规则

通过合理使用 useLayoutEffect 和 useEffect,我们可以更好地控制副作用的执行时机,优化用户体验,同时保持应用的性能。在实际开发中,应该根据具体场景选择合适的 Hook。


网站公告

今日签到

点亮在社区的每一天
去签到