- 发布于
React useEffect 钩子深度解析:副作用管理、依赖数组与性能优化
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
React useEffect 钩子深度解析:副作用管理、依赖数组与性能优化
useEffect 是 React Hooks 中最重要也是最复杂的钩子之一。它统一了类组件中的多个生命周期方法,提供了处理副作用的强大能力。本文将深入探讨 useEffect 的工作原理和最佳实践。
useEffect 基础概念
副作用的定义和类型
import React, { useState, useEffect } from 'react'
// 基础 useEffect 使用
function BasicEffectExample() {
const [count, setCount] = useState(0)
const [name, setName] = useState('')
// 1. 无依赖数组 - 每次渲染后都执行
useEffect(() => {
console.log('组件渲染完成')
document.title = `计数: ${count}`
})
// 2. 空依赖数组 - 只在挂载后执行一次
useEffect(() => {
console.log('组件挂载完成')
// 模拟 API 调用
fetch('/api/user')
.then((response) => response.json())
.then((data) => setName(data.name))
.catch((error) => console.error('获取用户信息失败:', error))
}, []) // 空依赖数组
// 3. 有依赖数组 - 只在依赖变化时执行
useEffect(() => {
console.log('count 发生变化:', count)
// 保存到本地存储
localStorage.setItem('count', count.toString())
}, [count]) // 依赖 count
// 4. 清理函数 - 处理副作用清理
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行')
}, 1000)
// 返回清理函数
return () => {
console.log('清理定时器')
clearInterval(timer)
}
}, [])
return (
<div>
<h2>用户: {name}</h2>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>增加计数</button>
</div>
)
}
useEffect 执行时机详解
import React, { useState, useEffect, useLayoutEffect } from 'react'
function ExecutionTimingDemo() {
const [count, setCount] = useState(0)
console.log('1. 组件渲染开始')
// useLayoutEffect - 同步执行,在 DOM 更新后立即执行
useLayoutEffect(() => {
console.log('3. useLayoutEffect 执行 - DOM 已更新但浏览器未绘制')
})
// useEffect - 异步执行,在浏览器绘制后执行
useEffect(() => {
console.log('4. useEffect 执行 - 浏览器已完成绘制')
})
console.log('2. 组件渲染结束,即将提交到 DOM')
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>点击查看执行顺序</button>
</div>
)
}
// 执行顺序演示
function LifecycleComparison() {
const [mounted, setMounted] = useState(false)
useEffect(() => {
console.log('useEffect: 组件挂载/更新后')
return () => {
console.log('useEffect cleanup: 组件卸载/依赖变化前')
}
})
useEffect(() => {
console.log('useEffect with empty deps: 仅在挂载后执行')
return () => {
console.log('useEffect cleanup with empty deps: 仅在卸载前执行')
}
}, [])
if (!mounted) {
return <button onClick={() => setMounted(true)}>挂载组件</button>
}
return (
<div>
<p>组件已挂载</p>
<button onClick={() => setMounted(false)}>卸载组件</button>
</div>
)
}
依赖数组深度解析
依赖数组的工作原理
import React, { useState, useEffect, useCallback, useMemo } from 'react'
function DependencyArrayDemo() {
const [count, setCount] = useState(0)
const [name, setName] = useState('John')
const [user, setUser] = useState({ id: 1, name: 'John' })
// 1. 基本类型依赖
useEffect(() => {
console.log('count 变化:', count)
}, [count])
// 2. 对象依赖 - 浅比较问题
useEffect(() => {
console.log('user 对象变化:', user)
// 注意:即使对象内容相同,如果是新对象引用,也会触发
}, [user])
// 3. 函数依赖 - 需要使用 useCallback
const handleUserUpdate = useCallback((newName) => {
setUser((prevUser) => ({ ...prevUser, name: newName }))
}, []) // 空依赖,函数不会重新创建
useEffect(() => {
console.log('handleUserUpdate 函数变化')
}, [handleUserUpdate])
// 4. 计算值依赖 - 使用 useMemo
const expensiveValue = useMemo(() => {
console.log('计算昂贵的值')
return count * 1000
}, [count])
useEffect(() => {
console.log('expensiveValue 变化:', expensiveValue)
}, [expensiveValue])
// 5. 多个依赖
useEffect(() => {
console.log('count 或 name 变化:', { count, name })
}, [count, name])
return (
<div>
<p>计数: {count}</p>
<p>姓名: {name}</p>
<p>用户: {JSON.stringify(user)}</p>
<p>昂贵计算值: {expensiveValue}</p>
<button onClick={() => setCount(count + 1)}>增加计数</button>
<button onClick={() => setName(name === 'John' ? 'Jane' : 'John')}>切换姓名</button>
<button onClick={() => handleUserUpdate(name)}>更新用户</button>
</div>
)
}
// 依赖数组常见陷阱
function DependencyTraps() {
const [count, setCount] = useState(0)
// ❌ 错误:缺少依赖
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // count 被闭包捕获,始终是初始值 0
}, 1000)
return () => clearInterval(timer)
}, []) // 缺少 count 依赖
// ✅ 正确:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount((prevCount) => prevCount + 1) // 使用函数式更新
}, 1000)
return () => clearInterval(timer)
}, []) // 不需要 count 依赖
// ✅ 正确:包含所有依赖
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(timer)
}, [count]) // 包含 count 依赖
return <div>计数: {count}</div>
}
自定义依赖比较
import React, { useState, useEffect, useRef } from 'react'
// 自定义 useEffect,支持深度比较
function useDeepEffect(callback, dependencies) {
const currentDepsRef = useRef()
const callbackRef = useRef(callback)
// 深度比较函数
function deepEqual(a, b) {
if (a === b) return true
if (a == null || b == null) return false
if (typeof a !== 'object' || typeof b !== 'object') return false
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (let key of keysA) {
if (!keysB.includes(key)) return false
if (!deepEqual(a[key], b[key])) return false
}
return true
}
// 更新回调引用
callbackRef.current = callback
useEffect(() => {
// 深度比较依赖
if (!deepEqual(currentDepsRef.current, dependencies)) {
currentDepsRef.current = dependencies
return callbackRef.current()
}
})
}
// 使用自定义 hook
function DeepComparisonDemo() {
const [user, setUser] = useState({ name: 'John', age: 30 })
const [settings, setSettings] = useState({ theme: 'light', lang: 'en' })
// 使用深度比较的 effect
useDeepEffect(() => {
console.log('用户信息深度变化:', user)
}, [user])
useDeepEffect(() => {
console.log('设置深度变化:', settings)
}, [settings])
const updateUserName = () => {
// 即使创建新对象,但内容相同,不会触发 effect
setUser({ name: 'John', age: 30 })
}
const updateUserAge = () => {
setUser((prev) => ({ ...prev, age: prev.age + 1 }))
}
return (
<div>
<p>用户: {JSON.stringify(user)}</p>
<p>设置: {JSON.stringify(settings)}</p>
<button onClick={updateUserName}>更新用户名(相同值)</button>
<button onClick={updateUserAge}>增加年龄</button>
</div>
)
}
// 使用 useRef 优化依赖
function OptimizedDependencies() {
const [data, setData] = useState([])
const [filter, setFilter] = useState('')
// 使用 ref 存储稳定的函数引用
const apiRef = useRef({
fetchData: async (filterValue) => {
const response = await fetch(`/api/data?filter=${filterValue}`)
return response.json()
},
})
useEffect(() => {
let cancelled = false
const loadData = async () => {
try {
const result = await apiRef.current.fetchData(filter)
if (!cancelled) {
setData(result)
}
} catch (error) {
if (!cancelled) {
console.error('加载数据失败:', error)
}
}
}
loadData()
return () => {
cancelled = true
}
}, [filter]) // 只依赖 filter,不依赖函数
return (
<div>
<input value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="过滤条件" />
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
}
实际应用场景
数据获取和 API 调用
import React, { useState, useEffect, useCallback } from 'react'
// 自定义数据获取 hook
function useApi(url, options = {}) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchData = useCallback(async () => {
try {
setLoading(true)
setError(null)
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
setData(result)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [url, JSON.stringify(options)])
useEffect(() => {
fetchData()
}, [fetchData])
const refetch = useCallback(() => {
fetchData()
}, [fetchData])
return { data, loading, error, refetch }
}
// 使用数据获取 hook
function UserProfile({ userId }) {
const { data: user, loading, error, refetch } = useApi(`/api/users/${userId}`)
const [posts, setPosts] = useState([])
// 获取用户文章
useEffect(() => {
if (!user) return
let cancelled = false
const fetchPosts = async () => {
try {
const response = await fetch(`/api/users/${user.id}/posts`)
const userPosts = await response.json()
if (!cancelled) {
setPosts(userPosts)
}
} catch (error) {
if (!cancelled) {
console.error('获取文章失败:', error)
}
}
}
fetchPosts()
return () => {
cancelled = true
}
}, [user])
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
if (!user) return <div>用户不存在</div>
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<h3>文章列表</h3>
{posts.map((post) => (
<div key={post.id}>
<h4>{post.title}</h4>
<p>{post.excerpt}</p>
</div>
))}
<button onClick={refetch}>刷新用户信息</button>
</div>
)
}
// 搜索功能实现
function SearchComponent() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
// 防抖搜索
useEffect(() => {
if (!query.trim()) {
setResults([])
return
}
const timeoutId = setTimeout(async () => {
setLoading(true)
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
const searchResults = await response.json()
setResults(searchResults)
} catch (error) {
console.error('搜索失败:', error)
setResults([])
} finally {
setLoading(false)
}
}, 500) // 500ms 防抖
return () => {
clearTimeout(timeoutId)
}
}, [query])
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
{loading && <div>搜索中...</div>}
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
)
}
事件监听和清理
import React, { useState, useEffect, useCallback } from 'react'
// 窗口尺寸监听
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
})
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
// 清理事件监听器
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return windowSize
}
// 键盘事件监听
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false)
useEffect(() => {
const downHandler = (event) => {
if (event.key === targetKey) {
setKeyPressed(true)
}
}
const upHandler = (event) => {
if (event.key === targetKey) {
setKeyPressed(false)
}
}
window.addEventListener('keydown', downHandler)
window.addEventListener('keyup', upHandler)
return () => {
window.removeEventListener('keydown', downHandler)
window.removeEventListener('keyup', upHandler)
}
}, [targetKey])
return keyPressed
}
// 鼠标位置跟踪
function useMousePosition() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMouseMove = (event) => {
setMousePosition({
x: event.clientX,
y: event.clientY,
})
}
document.addEventListener('mousemove', handleMouseMove)
return () => {
document.removeEventListener('mousemove', handleMouseMove)
}
}, [])
return mousePosition
}
// 使用自定义 hooks
function InteractiveComponent() {
const windowSize = useWindowSize()
const escapePressed = useKeyPress('Escape')
const mousePosition = useMousePosition()
const [showModal, setShowModal] = useState(false)
// ESC 键关闭模态框
useEffect(() => {
if (escapePressed && showModal) {
setShowModal(false)
}
}, [escapePressed, showModal])
// 在线状态监听
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
return (
<div>
<div>
窗口尺寸: {windowSize.width} x {windowSize.height}
</div>
<div>
鼠标位置: ({mousePosition.x}, {mousePosition.y})
</div>
<div>网络状态: {isOnline ? '在线' : '离线'}</div>
<div>ESC 键状态: {escapePressed ? '按下' : '释放'}</div>
<button onClick={() => setShowModal(true)}>打开模态框</button>
{showModal && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
background: 'white',
padding: '20px',
borderRadius: '8px',
}}
>
<p>按 ESC 键关闭模态框</p>
<button onClick={() => setShowModal(false)}>关闭</button>
</div>
</div>
)}
</div>
)
}
定时器和动画
import React, { useState, useEffect, useRef } from 'react'
// 倒计时 hook
function useCountdown(initialTime) {
const [time, setTime] = useState(initialTime)
const [isActive, setIsActive] = useState(false)
useEffect(() => {
let intervalId = null
if (isActive && time > 0) {
intervalId = setInterval(() => {
setTime((prevTime) => prevTime - 1)
}, 1000)
} else if (time === 0) {
setIsActive(false)
}
return () => {
if (intervalId) {
clearInterval(intervalId)
}
}
}, [isActive, time])
const start = () => setIsActive(true)
const pause = () => setIsActive(false)
const reset = () => {
setTime(initialTime)
setIsActive(false)
}
return { time, isActive, start, pause, reset }
}
// 动画 hook
function useAnimation(duration = 1000) {
const [progress, setProgress] = useState(0)
const [isAnimating, setIsAnimating] = useState(false)
const startTimeRef = useRef(null)
const requestRef = useRef(null)
const animate = useCallback(
(timestamp) => {
if (!startTimeRef.current) {
startTimeRef.current = timestamp
}
const elapsed = timestamp - startTimeRef.current
const newProgress = Math.min(elapsed / duration, 1)
setProgress(newProgress)
if (newProgress < 1) {
requestRef.current = requestAnimationFrame(animate)
} else {
setIsAnimating(false)
startTimeRef.current = null
}
},
[duration]
)
useEffect(() => {
if (isAnimating) {
requestRef.current = requestAnimationFrame(animate)
}
return () => {
if (requestRef.current) {
cancelAnimationFrame(requestRef.current)
}
}
}, [isAnimating, animate])
const startAnimation = () => {
setProgress(0)
setIsAnimating(true)
startTimeRef.current = null
}
return { progress, isAnimating, startAnimation }
}
// 使用定时器和动画
function TimerAnimationDemo() {
const { time, isActive, start, pause, reset } = useCountdown(60)
const { progress, isAnimating, startAnimation } = useAnimation(2000)
// 自动保存功能
const [content, setContent] = useState('')
const [lastSaved, setLastSaved] = useState(null)
useEffect(() => {
if (!content.trim()) return
const saveTimeout = setTimeout(() => {
// 模拟保存到服务器
console.log('自动保存内容:', content)
setLastSaved(new Date())
}, 2000) // 2秒后自动保存
return () => {
clearTimeout(saveTimeout)
}
}, [content])
return (
<div>
<div>
<h3>倒计时器</h3>
<div>剩余时间: {time} 秒</div>
<button onClick={start} disabled={isActive}>
开始
</button>
<button onClick={pause} disabled={!isActive}>
暂停
</button>
<button onClick={reset}>重置</button>
</div>
<div>
<h3>动画进度</h3>
<div
style={{
width: '200px',
height: '20px',
background: '#f0f0f0',
borderRadius: '10px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${progress * 100}%`,
height: '100%',
background: '#4caf50',
transition: 'width 0.1s ease',
}}
/>
</div>
<div>进度: {Math.round(progress * 100)}%</div>
<button onClick={startAnimation} disabled={isAnimating}>
开始动画
</button>
</div>
<div>
<h3>自动保存</h3>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="输入内容,2秒后自动保存..."
rows={4}
cols={50}
/>
{lastSaved && <div>最后保存时间: {lastSaved.toLocaleTimeString()}</div>}
</div>
</div>
)
}
性能优化和最佳实践
避免不必要的重新渲染
import React, { useState, useEffect, useMemo, useCallback } from 'react'
// 性能优化示例
function OptimizedComponent() {
const [count, setCount] = useState(0)
const [users, setUsers] = useState([])
const [filter, setFilter] = useState('')
// ✅ 使用 useMemo 缓存计算结果
const filteredUsers = useMemo(() => {
console.log('过滤用户列表') // 只在 users 或 filter 变化时执行
return users.filter((user) => user.name.toLowerCase().includes(filter.toLowerCase()))
}, [users, filter])
// ✅ 使用 useCallback 缓存函数
const handleUserClick = useCallback((userId) => {
console.log('用户点击:', userId)
// 处理用户点击
}, [])
// ✅ 合并相关的 effect
useEffect(() => {
// 同时处理多个相关的副作用
document.title = `计数: ${count}`
localStorage.setItem('count', count.toString())
// 发送分析数据
if (count > 0) {
analytics.track('count_changed', { count })
}
}, [count])
// ✅ 条件性执行 effect
useEffect(() => {
if (filter.length < 2) return // 过滤条件太短时不执行
const timeoutId = setTimeout(() => {
// 执行搜索
searchUsers(filter)
}, 300)
return () => clearTimeout(timeoutId)
}, [filter])
return (
<div>
<div>计数: {count}</div>
<button onClick={() => setCount(count + 1)}>增加</button>
<input value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="过滤用户" />
<UserList users={filteredUsers} onUserClick={handleUserClick} />
</div>
)
}
// 错误处理和清理
function RobustEffectComponent() {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
const abortController = new AbortController()
const fetchData = async () => {
try {
setError(null)
const response = await fetch('/api/data', {
signal: abortController.signal,
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const result = await response.json()
if (!cancelled) {
setData(result)
}
} catch (err) {
if (!cancelled && err.name !== 'AbortError') {
setError(err.message)
}
}
}
fetchData()
return () => {
cancelled = true
abortController.abort()
}
}, [])
if (error) return <div>错误: {error}</div>
if (!data) return <div>加载中...</div>
return <div>数据: {JSON.stringify(data)}</div>
}
总结
React useEffect 钩子的核心要点和最佳实践:
🎯 核心概念
- 副作用管理:统一处理组件的副作用
- 执行时机:渲染后异步执行
- 依赖数组:控制 effect 的执行条件
- 清理函数:处理副作用的清理工作
✅ 最佳实践
- 正确设置依赖数组
- 使用清理函数防止内存泄漏
- 合理使用 useCallback 和 useMemo
- 处理异步操作的取消
- 避免在 effect 中直接修改 DOM
⚠️ 常见陷阱
- 缺少依赖导致的闭包问题
- 无限循环的 effect 执行
- 忘记清理定时器和事件监听器
- 在 effect 中进行不必要的计算
🚀 性能优化
- 使用条件判断避免不必要的执行
- 合并相关的 effect
- 使用 AbortController 取消请求
- 正确使用依赖数组
掌握 useEffect,你就能优雅地处理 React 组件中的所有副作用!
useEffect 是 React Hooks 的核心,理解它的工作原理是掌握现代 React 开发的关键。