在 React 函数组件中,副作用(side effects)操作通常通过 useEffect 或 useLayoutEffect 这两个 Hook 来实现。它们的用法类似,但执行时机和应用场景有所不同。
一、概念详解
什么是副作用?
副作用指的是那些不属于组件渲染本身的操作,比如:
- 数据请求(如 fetch、axios)
- 事件监听(如 window.addEventListener)
- 手动操作 DOM(如 document.querySelector、ref.current)
- 启动/清除定时器(如 setTimeout、setInterval)
- 订阅/取消订阅外部服务
这些操作通常需要在组件渲染后进行,并且在组件卸载或依赖变化时进行清理。
useEffect 的用法
useEffect 是最常用的副作用 Hook。它的基本语法如下:
import { useEffect } from 'react';
useEffect(() => {
// 这里写副作用逻辑,比如请求数据、操作 DOM、添加事件监听等
return () => {
// 这里写清理逻辑,比如移除事件监听、清除定时器等(可选)
}
}, [依赖项]);
- 第一个参数是一个函数,函数体内写副作用代码。
- 该函数可以返回一个清理函数(可选),用于组件卸载或依赖变化时执行。
- 第二个参数是依赖数组,只有依赖变化时副作用才会重新执行。
常见用法举例:
- 只在组件挂载和卸载时执行(类似 componentDidMount/componentWillUnmount):
useEffect(() => {
// 只执行一次
return () => {
// 组件卸载时清理
}
}, []);
- 依赖某个状态或 props 变化时执行:
useEffect(() => {
// 依赖 count,每次 count 变化时执行
}, [count]);
useLayoutEffect 的用法
useLayoutEffect 的用法和 useEffect 完全一致,唯一的区别在于它的执行时机:
import { useLayoutEffect } from 'react';
useLayoutEffect(() => {
// 这里写需要同步执行的副作用逻辑
return () => {
// 清理逻辑(可选)
}
}, [依赖项]);
注意:
useLayoutEffect会在 DOM 更新后、浏览器绘制前同步执行。- 这意味着它会阻塞页面渲染,直到副作用执行完毕。
二、useEffect 与 useLayoutEffect 的异同
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 组件渲染到屏幕后,异步执行 | 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()),然后根据这些信息做动画初始化。
-
必须保证动画初始化和页面显示是同步的:如果动画的起始位置依赖于 DOM 的尺寸或位置,必须在页面显示前就把动画准备好,否则用户会看到“闪烁”或“跳动”。
-
用 useLayoutEffect 可以保证读取到的 DOM 信息是最新的,并且动画初始化在页面显示前就完成了。
3. 事件监听与清理(用 useEffect)
思路:
比如监听窗口大小变化,推荐用 useEffect,并在清理函数中移除监听。
实现:
import { useEffect } from 'react';
function Example() {
useEffect(() => {
const handleResize = () => {
// 处理窗口大小变化
};
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 只在挂载和卸载时执行
// ...
}
在这个例子里,我们想在组件挂载时监听窗口大小变化(resize 事件),并在组件卸载时移除监听,防止内存泄漏。
-
事件监听不需要阻塞页面渲染:添加或移除事件监听,不会影响页面的显示速度,也不需要在页面显示前就完成。
-
用 useEffect 更高效,因为 React 会先把页面渲染出来,然后再添加事件监听,用户体验不会受到影响。
-
清理函数很重要:useEffect 的返回值是一个清理函数,这里用来移除事件监听,防止组件卸载后还在响应事件,造成内存泄漏或报错。
四、选取思路总结
-
优先考虑 useEffect
- 大多数副作用(如数据请求、事件监听、定时器、普通 DOM 操作等)都可以用
useEffect实现。 useEffect在页面渲染后异步执行,不会影响页面的首次显示速度,用户体验更好。- React 官方也推荐,除非有特殊需求,优先用
useEffect。
- 大多数副作用(如数据请求、事件监听、定时器、普通 DOM 操作等)都可以用
-
只有在必须“同步”操作 DOM 时才用 useLayoutEffect
- 如果你的副作用需要在页面显示前就完成,比如:
- 读取或测量 DOM 元素的尺寸、位置(如动画的起始点、布局计算等)
- 需要同步修改 DOM,保证用户看到的页面是“最终状态”,避免闪烁或跳动
- 这时用
useLayoutEffect,它会在 DOM 更新后、浏览器绘制前同步执行,确保操作和页面显示同步。
- 如果你的副作用需要在页面显示前就完成,比如:
-
性能和体验的权衡
useLayoutEffect会阻塞页面渲染,副作用执行完页面才会显示,如果滥用会影响性能。- 所以,只有在真的需要“同步”操作 DOM 时才用它,否则都用
useEffect。
-
清理副作用很重要
- 无论用哪个 Hook,都要记得在副作用中返回清理函数(如移除事件监听、清除定时器等),防止内存泄漏和潜在 bug。
-
实际开发中的常见选择
- 用
useEffect的场景:数据请求、事件监听、定时器、普通样式修改等。 - 用
useLayoutEffect的场景:动画初始化、布局测量、需要同步读取/修改 DOM 的场景。
- 用
一句话概括
“能用 useEffect 就用 useEffect,只有必须同步操作 DOM 时才用 useLayoutEffect。”
-- 如果你不确定用哪个,先用 useEffect,只有发现页面有闪烁、动画不流畅等问题时,再考虑用 useLayoutEffect。