主题
useEffect
介绍
useEffect 是 React 中的一个 Hook,用于在函数组件中处理副作用。副作用包括数据获取、订阅、手动修改 DOM 以及其他需要在组件渲染后执行的操作。
基本使用
语法
js
useEffect(setup, dependencies?)参数:
setup:setup是一个函数,该函数定义了要执行的副作用代码dependencies(可选):dependencies是依赖数组,用于控制副作用的执行时机。
用法
无依赖数组
如果没有第二个参数,useEffect 会在每次渲染后执行:
js
useEffect(() => {
console.log('每次渲染后都会执行')
})提示
可以类比于 vue 中的 onUpdated 生命周期钩子。
空依赖数组 []
如果传入空数组,useEffect 只会在组件首次渲染时执行一次。
jsx
import { useState, useEffect } from 'react'
function App() {
const [pageTitle, setPageTitle] = useState('')
useEffect(() => {
setPageTitle(document.title)
}, [])
return <p>Page title: {pageTitle}</p>
}
export default App提示
可以类比于 vue 中的 onMounted 生命周期钩子。
有依赖数组 [dep1, dep2, ...]
可以在数组中指定多个依赖项。只有当数组中的依赖发生变化时,useEffect 才会重新执行。
jsx
import { useState, useEffect } from 'react'
function App() {
const [count] = useState(10)
const [doubleCount, setDoubleCount] = useState(count * 2)
useEffect(() => {
setDoubleCount(count * 2)
}, [count])
return <div>{doubleCount}</div>
}
export default App提示
可以类比于 vue 中的 watch 侦听器。
清理副作用
在副作用中,如果涉及到订阅、计时器等需要清理的操作,可以返回一个清理函数。这个清理函数会在组件卸载或下次执行副作用之前运行:
jsx
import { useState, useEffect } from 'react'
function App() {
const [time, setTime] = useState(new Date().toLocaleTimeString())
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date().toLocaleTimeString())
}, 1000)
return () => clearInterval(timer)
}, [])
return <p>Current time: {time}</p>
}
export default App提示
清理函数 可以类比于 vue 中的 onUnmounted 生命周期钩子。
useEffect 与 useLayoutEffect 的执行时机?
React 完整的渲染周期:
- 状态变化触发
- 渲染阶段
- 计算虚拟 DOM
- 提交阶段
- 更新真实 DOM
- useLayoutEffect 执行
- 浏览器绘制
- useEffect 执行
useEffect 执行时机
useEffect 不阻塞浏览器渲染的副作用渲染
- 触发渲染:组件状态(state)或者 props 变化
- 渲染阶段:计算新的虚拟 DOM
- 提交阶段:更新真实 DOM
- 浏览器绘制:用户看到更新后的界面
- useEffect 执行:异步处理
useLayoutEffect 执行时机
useLayoutEffect 在浏览器绘制前的精准控制
- 触发渲染:组件状态(state)或者 props 变化
- 渲染阶段:计算新的虚拟 DOM
- 提交阶段:更新真实 DOM
- useLayoutEffect 执行:同步处理
- 浏览器绘制:用户看到更新后的界面
核心区别
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 浏览器绘制后 | 浏览器绘制前 |
| 执行方式 | 异步 | 同步 |
| 对渲染的影响 | 不阻塞渲染 | 阻塞渲染 |
| 适用场景 | 数据获取、事件监听、订阅等副作用 | DOM 测量、布局调整、避免闪烁 |
useEffect 依赖项有什么用?
常见的副作用场景:
- 数据获取:从服务端获取数据
- DOM 操作:手动修改 DOM
- 事件监听:添加和移除事件监听器
- 定时器:设置和清除定时器
- ...
这就引出一个关键问题,这些副作用什么时候执行?
依赖项数组的作用就是告诉 React 何时重新执行副作用:
- 无数组:每次渲染后都执行
- 空数组
[]:只在组件挂载和卸载时执行一次 - 有数组
[dep1, dep2]:只有当dep1或dep2发生变化时才会执行
其比较原理就是使用 Object.is 来进行逐项比较。
❌ 常见陷阱:对象依赖项
options 每次渲染都是新对象,导致 Effect 重复执行:
jsx
function Counter() {
const [count, setCount] = useState(0)
// 每次渲染都会重新创建对象
const options = { step: 1 }
useEffect(() => {
console.log('Effect executed')
}, [options])
}解决方案:
jsx
const options = { step: 1 }
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('Effect executed')
}, [options]) // 现在 options 是稳定的
}jsx
function Counter() {
const [step, setStep] = useState(1)
// 使用 useMemo 来稳定对象引用
const options = useMemo(() => ({ step }), [step])
useEffect(() => {
console.log('Effect executed')
}, [options]) // 现在 options 是稳定的
}❌ 常见陷阱:函数依赖项
jsx
function SearchComponent({ query }) {
const [results, setResults] = useState([])
// 每次渲染都会重新创建函数
const searchHandler = (searchQuery) => {
return fetch(`/api/search?q=${searchQuery}`)
.then((res) => res.json())
.then((data) => setResults(data.items))
}
useEffect(() => {
searchHandler(query)
}, [query, searchHandler])
}解决方案:
jsx
function SearchComponent({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
const searchHandler = (searchQuery) => {
return fetch(`/api/search?q=${searchQuery}`)
.then((res) => res.json())
.then((data) => setResults(data.items))
}
searchHandler(query)
}, [query]) // 现在不依赖于外部函数
}jsx
function SearchComponent({ query }) {
const [results, setResults] = useState([])
// 使用 useCallback 来稳定函数引用
const searchHandler = useCallback((searchQuery) => {
return fetch(`/api/search?q=${searchQuery}`)
.then((res) => res.json())
.then((data) => setResults(data.items))
}, []) // 函数不依赖于任何外部变量
useEffect(() => {
searchHandler(query)
}, [query, searchHandler]) // 现在searchHandler 是稳定的
}useEffect 中处理异步请求
❌ 错误示例:直接使用 async / await
jsx
useEffect(async () => {
const result = await fetchData(id)
setData(result)
}, [id])为什么不行?
- 违反了 useEffect 规则:
- useEffect 要不返回任何东西,要么返回清理函数。
- async 函数隐式返回 Promise,违反设计规则。
- 没有解决“竞态条件”
- 仍然可能出现请求顺序错乱的问题
- 组件卸载后任然可能尝试更新状态
解决方案:
jsx
useEffect(() => {
// 1. 设置标记
let isMounted = true
;(async () => {
const result = await fetchData(id)
// 3. 更新状态前检查标记
if (isMounted) {
setData(result)
}
})()
return () => {
// 2. 清理时重置标记
isMounted = false
}
}, [id])jsx
useEffect(() => {
// 1. 创建 AbortController 实例
const controller = new AbortController()
;(async () => {
try {
const result = await fetchData(
id,
// 2. 传递 signal 用于取消请求
{ signal: controller.signal },
)
setData(result)
} catch (error) {
if (error.name === 'AbortError') {
// 请求被终止是预期行为
} else {
// 处理其他错误
}
}
})()
return () => {
// 3. 清理函数中终止请求
controller.abort()
}
}, [id])