React 副作用 Hook:useEffect 与 useLayoutEffect的异同

181 阅读6分钟

在 React 函数组件中,副作用(side effects)操作通常通过 useEffectuseLayoutEffect 这两个 Hook 来实现。它们的用法类似,但执行时机和应用场景有所不同。

一、概念详解

什么是副作用?

副作用指的是那些不属于组件渲染本身的操作,比如:

  • 数据请求(如 fetch、axios)
  • 事件监听(如 window.addEventListener)
  • 手动操作 DOM(如 document.querySelector、ref.current)
  • 启动/清除定时器(如 setTimeout、setInterval)
  • 订阅/取消订阅外部服务

这些操作通常需要在组件渲染后进行,并且在组件卸载或依赖变化时进行清理。

useEffect 的用法

useEffect 是最常用的副作用 Hook。它的基本语法如下:

import { useEffect } from 'react';

useEffect(() => {
  // 这里写副作用逻辑,比如请求数据、操作 DOM、添加事件监听等

  return () => {
    // 这里写清理逻辑,比如移除事件监听、清除定时器等(可选)
  }
}, [依赖项]);
  • 第一个参数是一个函数,函数体内写副作用代码。
  • 该函数可以返回一个清理函数(可选),用于组件卸载或依赖变化时执行。
  • 第二个参数是依赖数组,只有依赖变化时副作用才会重新执行。

常见用法举例:

  1. 只在组件挂载和卸载时执行(类似 componentDidMount/componentWillUnmount):
useEffect(() => {
  // 只执行一次
  return () => {
    // 组件卸载时清理
  }
}, []);
  1. 依赖某个状态或 props 变化时执行:
useEffect(() => {
  // 依赖 count,每次 count 变化时执行
}, [count]);

useLayoutEffect 的用法

useLayoutEffect 的用法和 useEffect 完全一致,唯一的区别在于它的执行时机:

import { useLayoutEffect } from 'react';

useLayoutEffect(() => {
  // 这里写需要同步执行的副作用逻辑

  return () => {
    // 清理逻辑(可选)
  }
}, [依赖项]);

注意:

  1. useLayoutEffect 会在 DOM 更新后、浏览器绘制前同步执行。
  2. 这意味着它会阻塞页面渲染,直到副作用执行完毕。

二、useEffect 与 useLayoutEffect 的异同

特性useEffectuseLayoutEffect
执行时机组件渲染到屏幕后,异步执行DOM 更新后、浏览器绘制前,同步执行
是否阻塞渲染否,不会阻塞页面渲染是,会阻塞页面渲染
典型应用场景数据请求、事件监听、异步 DOM 操作读取/同步修改 DOM、布局测量、动画
性能影响较小,推荐优先使用可能影响性能,除非必要不建议使用
依赖数组支持支持

形象理解

对于useEffect来说,它更像是“渲染完成后再做点事”,不会影响用户看到页面的速度。

useLayoutEffect则更像是“渲染完马上做点事,做完了用户才能看到页面”,适合需要精确操作 DOM 的场景。

详细流程图

sequenceDiagram
    participant 组件
    participant DOM
    participant 浏览器
    组件->>DOM: 渲染
    DOM->>组件: 渲染完成
    组件->>组件: useLayoutEffect 执行(同步,阻塞渲染)
    组件->>浏览器: 浏览器绘制
    组件->>组件: useEffect 执行(异步,渲染后)

三、实战示例与分步讲解

给出如下代码引入情景:

import { useState } from 'react'
import './App.css'
import Page from './components/Page'
import { ThemeContext } from './ThemeContext'

function App() {
  const [theme, setTheme] = useState("light");
  const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <Page />
    </ThemeContext.Provider>
  )
}

export default App

1. 主题切换时修改 body 背景色(用 useEffect)

思路:
切换主题时,我们希望 <body> 的背景色跟着变化。

实现:

import { useEffect, useState } from 'react';

function App() {
  const [theme, setTheme] = useState("light");
  const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');

  useEffect(() => {
    document.body.style.background = theme === 'light' ? '#fff' : '#222';
    // 清理函数,防止主题切换时遗留样式
    return () => {
      document.body.style.background = '';
    }
  }, [theme]);

  // ...其余代码
}

为什么这里用 useEffect?

在这个例子里,我们想根据主题(theme)切换,动态修改body的背景色。这属于操作 DOM,但它的结果(背景色变化)并不需要在页面渲染前就完成,只要页面渲染后再改就可以了。

1.不会影响页面显示速度:用 useEffect,React 会先把页面渲染出来,然后再去改背景色,用户几乎感受不到延迟。

2.不会阻塞渲染:如果用 useLayoutEffect,会让 React 等你改完背景色再显示页面,没必要。

2.需要同步测量 DOM 或实现无闪烁动画

思路:
假如你在 Page 组件中有动画,动画的起始位置依赖于 DOM 的尺寸或位置,这时就要用 useLayoutEffect,否则动画可能会闪烁。

实现:

import { useLayoutEffect, useRef } from 'react';

function Page() {
  const ref = useRef();

  useLayoutEffect(() => {
    const rect = ref.current.getBoundingClientRect();
    // 根据 rect 做动画初始化,保证动画不会闪烁
  }, []);

  return <div ref={ref}>内容</div>
}

为什么这里用 useLayoutEffect?

在这个例子里,我们想在组件渲染后,立即获取 DOM 元素的尺寸和位置(getBoundingClientRect()),然后根据这些信息做动画初始化。

  1. 必须保证动画初始化和页面显示是同步的:如果动画的起始位置依赖于 DOM 的尺寸或位置,必须在页面显示前就把动画准备好,否则用户会看到“闪烁”或“跳动”。

  2. 用 useLayoutEffect 可以保证读取到的 DOM 信息是最新的,并且动画初始化在页面显示前就完成了。

3. 事件监听与清理(用 useEffect)

思路:
比如监听窗口大小变化,推荐用 useEffect,并在清理函数中移除监听。

实现:

import { useEffect } from 'react';

function Example() {
  useEffect(() => {
    const handleResize = () => {
      // 处理窗口大小变化
    };
    window.addEventListener('resize', handleResize);

    // 清理函数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 只在挂载和卸载时执行
  // ...
}

在这个例子里,我们想在组件挂载时监听窗口大小变化(resize 事件),并在组件卸载时移除监听,防止内存泄漏。

  1. 事件监听不需要阻塞页面渲染:添加或移除事件监听,不会影响页面的显示速度,也不需要在页面显示前就完成。

  2. 用 useEffect 更高效,因为 React 会先把页面渲染出来,然后再添加事件监听,用户体验不会受到影响。

  3. 清理函数很重要:useEffect 的返回值是一个清理函数,这里用来移除事件监听,防止组件卸载后还在响应事件,造成内存泄漏或报错。

四、选取思路总结

  1. 优先考虑 useEffect

    • 大多数副作用(如数据请求、事件监听、定时器、普通 DOM 操作等)都可以用 useEffect 实现。
    • useEffect 在页面渲染后异步执行,不会影响页面的首次显示速度,用户体验更好。
    • React 官方也推荐,除非有特殊需求,优先用 useEffect
  2. 只有在必须“同步”操作 DOM 时才用 useLayoutEffect

    • 如果你的副作用需要在页面显示前就完成,比如:
      • 读取或测量 DOM 元素的尺寸、位置(如动画的起始点、布局计算等)
      • 需要同步修改 DOM,保证用户看到的页面是“最终状态”,避免闪烁或跳动
    • 这时用 useLayoutEffect,它会在 DOM 更新后、浏览器绘制前同步执行,确保操作和页面显示同步。
  3. 性能和体验的权衡

    • useLayoutEffect 会阻塞页面渲染,副作用执行完页面才会显示,如果滥用会影响性能
    • 所以,只有在真的需要“同步”操作 DOM 时才用它,否则都用 useEffect
  4. 清理副作用很重要

    • 无论用哪个 Hook,都要记得在副作用中返回清理函数(如移除事件监听、清除定时器等),防止内存泄漏和潜在 bug。
  5. 实际开发中的常见选择

    • useEffect 的场景:数据请求、事件监听、定时器、普通样式修改等。
    • useLayoutEffect 的场景:动画初始化、布局测量、需要同步读取/修改 DOM 的场景。

一句话概括

能用 useEffect 就用 useEffect,只有必须同步操作 DOM 时才用 useLayoutEffect。

-- 如果你不确定用哪个,先用 useEffect,只有发现页面有闪烁、动画不流畅等问题时,再考虑用 useLayoutEffect。