MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React useLayoutEffect 与 useEffect 的区别

2022-03-104.3k 阅读

React 中的副作用与 Hook

在 React 应用开发中,我们常常需要处理一些与组件渲染不直接相关但又依赖于组件状态或生命周期的操作,这类操作被称为“副作用(Side Effects)”。比如数据获取、订阅事件、手动操作 DOM 等。在 React Hook 出现之前,我们使用类组件的生命周期方法来处理副作用,像 componentDidMountcomponentDidUpdatecomponentWillUnmount。而 Hook 的出现,让函数组件也能够优雅地处理副作用,useEffectuseLayoutEffect 就是两个用于处理副作用的 Hook。

useEffect 详解

useEffect 是 React 提供的一个用于在函数组件中处理副作用的 Hook。它接收两个参数:一个是包含副作用操作的回调函数,另一个是可选的依赖数组。

useEffect 的执行时机

useEffect 会在组件渲染到 DOM 之后(异步地)执行。这意味着它不会阻塞浏览器渲染页面,保证了用户界面的流畅性。每当组件重新渲染时,useEffect 都会重新执行(除非依赖数组中的值没有变化)。

代码示例

import React, { useEffect } from 'react';

function Example() {
  const [count, setCount] = React.useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
    return () => {
      // 清理函数,在组件卸载或下一次 useEffect 执行前调用
      document.title = 'Default Title';
    };
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

在上述代码中,useEffect 回调函数会在每次 count 状态更新后执行,将页面标题设置为点击次数。同时,返回的清理函数会在组件卸载或 count 再次更新时执行,将标题重置为默认值。

依赖数组的作用

依赖数组决定了 useEffect 何时重新执行。如果依赖数组为空 [],那么 useEffect 只会在组件挂载和卸载时执行,类似于类组件中的 componentDidMountcomponentDidUnmount。如果依赖数组包含某些值,那么只有当这些值发生变化时,useEffect 才会重新执行。

useLayoutEffect 详解

useLayoutEffectuseEffect 非常相似,同样接收一个回调函数和一个可选的依赖数组。

useLayoutEffect 的执行时机

useLayoutEffect 会在所有 DOM 变更之后、浏览器绘制之前同步执行。这意味着它会阻塞浏览器渲染,直到其回调函数执行完毕。如果在 useLayoutEffect 中执行了大量计算,可能会导致页面卡顿。

代码示例

import React, { useLayoutEffect, useState } from'react';

function LayoutEffectExample() {
  const [width, setWidth] = useState(window.innerWidth);

  useLayoutEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      <p>The window width is: {width}px</p>
    </div>
  );
}

在这个例子中,useLayoutEffect 用于监听窗口大小变化并更新组件状态。由于它是在 DOM 变更后立即执行,所以能够准确获取最新的 DOM 布局信息,从而及时更新窗口宽度。

两者区别的本质分析

  1. 执行时机:这是两者最核心的区别。useEffect 异步执行,不会阻塞浏览器渲染,适用于大多数副作用场景,如数据获取、设置定时器等。而 useLayoutEffect 同步执行,在 DOM 变更后、浏览器绘制前执行,适合需要读取最新 DOM 布局并同步更新 DOM 的场景,比如测量元素尺寸、立即更新样式以避免闪烁等。
  2. 性能影响:由于 useLayoutEffect 阻塞渲染,如果其中执行了复杂计算,会导致页面卡顿,影响用户体验。因此,在使用 useLayoutEffect 时,应尽量确保其回调函数内的操作简单且高效。而 useEffect 由于异步执行,通常不会对渲染性能产生直接影响,但如果在 useEffect 中发起大量异步请求或执行频繁的 DOM 操作,也可能间接影响性能。
  3. 适用场景
    • useEffect
      • 数据获取:从 API 获取数据,因为这是异步操作,不会阻塞渲染。
      • 订阅和取消订阅:例如订阅浏览器事件、WebSocket 连接等,只要不需要立即更新 DOM 布局。
    • useLayoutEffect
      • 测量和调整 DOM:比如根据元素尺寸调整其位置或样式,需要在渲染前确保布局已更新。
      • 避免视觉闪烁:当根据组件状态立即更新 DOM 样式时,使用 useLayoutEffect 可以防止用户看到中间闪烁状态。

复杂场景下的选择

  1. 动画场景
    • 如果是基于时间的动画,如 setTimeout 控制的动画,使用 useEffect 更为合适。因为动画通常不需要立即同步 DOM 布局,异步执行不会影响动画的流畅性。
    • 对于基于 DOM 布局的动画,如根据元素尺寸变化进行的动画,useLayoutEffect 可能更适合。例如,当一个元素的高度动态变化时,需要立即根据新高度调整其下方元素的位置,useLayoutEffect 可以确保在浏览器绘制前完成这些调整,避免动画过程中的布局跳动。
  2. 数据获取与 DOM 操作结合场景
    • 假设一个组件需要从 API 获取数据,然后根据数据更新 DOM 元素的样式。如果数据获取过程较慢,且样式更新不需要立即呈现给用户(即可以接受一定的延迟),使用 useEffect 即可。这样可以保证在数据获取过程中页面仍然可交互,不会阻塞渲染。
    • 但如果数据获取后需要立即、精确地更新 DOM 布局,以确保用户看到的是连贯的界面,比如根据获取的数据动态调整元素的位置和大小,此时 useLayoutEffect 可能更合适。不过要注意,由于数据获取可能是异步的,需要在 useLayoutEffect 中进行适当的条件判断,避免在数据未获取到之前就尝试更新 DOM 而导致错误。

实际项目中的注意事项

  1. 避免过度使用:无论是 useEffect 还是 useLayoutEffect,都不应过度使用。过多的副作用操作会使组件逻辑变得复杂,难以维护。尽量将副作用操作封装成独立的函数或自定义 Hook,提高代码的可复用性和可读性。
  2. 依赖数组的准确性:在使用 useEffectuseLayoutEffect 时,依赖数组的设置至关重要。不准确的依赖数组可能导致副作用执行频率不当,比如遗漏依赖导致副作用未及时更新,或者依赖过多导致不必要的重复执行。在开发过程中,应仔细分析副作用操作依赖的状态和 props,确保依赖数组的准确性。
  3. 错误处理:在副作用回调函数中,应注意错误处理。由于 useEffectuseLayoutEffect 中的代码可能异步执行或在不同的生命周期阶段执行,传统的 try - catch 块可能无法捕获所有错误。可以使用 try - catch 结合 console.error 来处理和记录错误,确保应用的稳定性。

自定义 Hook 中的 useEffect 和 useLayoutEffect

在自定义 Hook 中使用 useEffectuseLayoutEffect 时,遵循的原则与在普通函数组件中类似。自定义 Hook 可以将一些重复的副作用逻辑封装起来,提高代码的复用性。

代码示例

import React, { useEffect, useState } from'react';

// 自定义 Hook
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return width;
}

function CustomHookExample() {
  const windowWidth = useWindowWidth();

  return (
    <div>
      <p>The window width from custom hook is: {windowWidth}px</p>
    </div>
  );
}

在上述代码中,useWindowWidth 自定义 Hook 使用 useEffect 来监听窗口大小变化。在 CustomHookExample 组件中,通过调用这个自定义 Hook 可以方便地获取窗口宽度。如果这个逻辑涉及到需要立即更新 DOM 布局的情况,也可以将 useEffect 替换为 useLayoutEffect

与类组件生命周期的对比

  1. useEffect 与类组件生命周期
    • useEffect 结合空依赖数组 [] 类似于类组件中的 componentDidMountcomponentWillUnmountuseEffect 回调函数中的操作对应 componentDidMount,返回的清理函数对应 componentWillUnmount
    • useEffect 依赖数组包含某些值时,类似 componentDidUpdate,但 useEffect 更加灵活,不需要像类组件那样在 componentDidUpdate 中手动比较 prevProps 和 nextProps 或 prevState 和 nextState。
  2. useLayoutEffect 与类组件生命周期:在类组件中没有完全对应的生命周期方法。useLayoutEffect 的同步执行特性在类组件中较难模拟,因为类组件的生命周期方法大多是异步执行的。不过,如果在 componentDidMountcomponentDidUpdate 中立即执行 DOM 操作且需要确保布局已更新,useLayoutEffect 可以提供类似的功能,但要注意其阻塞渲染的特性。

总结两者区别的实际应用建议

  1. 优先使用 useEffect:在大多数情况下,useEffect 能够满足需求。因为它不会阻塞渲染,对性能影响较小。只有在明确需要在 DOM 变更后立即同步更新 DOM 布局,或者避免视觉闪烁等场景下,才考虑使用 useLayoutEffect
  2. 性能测试与优化:在实际项目中,尤其是在性能敏感的应用中,应对使用 useEffectuseLayoutEffect 的组件进行性能测试。可以使用浏览器的性能分析工具,如 Chrome DevTools 的 Performance 面板,来分析组件渲染和副作用执行的时间,找出性能瓶颈并进行优化。如果发现 useLayoutEffect 导致了性能问题,可以尝试优化其中的操作,或者将部分操作移到 useEffect 中异步执行。
  3. 代码审查:在团队开发中,进行代码审查时应关注 useEffectuseLayoutEffect 的使用。检查依赖数组是否准确,确保副作用操作的执行时机和频率符合预期,避免因不当使用导致的逻辑错误和性能问题。

通过深入理解 useEffectuseLayoutEffect 的区别,并在实际项目中合理应用,开发者能够更好地控制组件的副作用,提高 React 应用的性能和用户体验。无论是简单的 UI 交互还是复杂的数据处理与 DOM 操作,选择合适的 Hook 是实现高效、流畅应用的关键之一。同时,随着 React 技术的不断发展,对这些基础概念的深入掌握也有助于开发者更快地适应新的特性和最佳实践。