React组件的生命周期与useEffect
现实应用需要与外部世界交互:获取数据、订阅事件、手动操作DOM等。这些操作在函数式编程中被称为"副作用"(side effects),而useEffect
正是处理这些副作用的函数
一、useEffect深度解析
useEffect
是React Hooks中最重要的钩子之一,它允许我们在函数组件中执行副作用操作。"副作用"指的是任何会与函数外部世界交互的操作。具体来说,当函数执行时除了返回一个值之外,还做了其他事情,我们就说这个函数产生了副作用。
比如修改DOM,设置定时器,记录日志等。
基本用法
-
无依赖数组,没有设置第二个参数,每次渲染后都执行
jsxuseEffect(() => { // 每次渲染后都会执行(包括首次) console.log('无依赖数组effect执行') })
-
空依赖数组,第二个参数为空数组,useEffect仅在挂载时执行
jsxuseEffect(() => { // 只在组件挂载时执行一次 console.log('空依赖数组effect执行') }, []);
-
有依赖项,当依赖项变化时执行
jsxconst [count, setCount] = useState(0); useEffect(() => { console.log('有依赖项effect执行') }, [count]);
组件首次渲染
数据count发生改变,第二次渲染,其中空依赖数组effect没有执行
清理机制
useEffect
可以返回一个清理函数,这个函数会在组件卸载时执行 ,即当组件从 DOM 中移除时,React 会执行所有 effect 的清理函数;还会在依赖项变化时执行,即当 effect 的依赖项发生变化,React 会先执行上一次 effect 的清理函数,再运行新的 effect
jsx
const [time,setTime] = useState(0)
useEffect(()=>{
console.log('定时器开启')
const timer = setInterval(() => {
setTime(prevTime => prevTime+1)
}, 1000)
return () => {
console.log('定时器关闭')
clearInterval(timer)
}
},[])
上述例子在 useEffect
中设置了一个每秒更新一次的定时器。如果不添加清理函数,当用户离开这个页面时,组件从DOM树中移除了,但是定时器还在运行。这样不仅浪费系统资源,还会更新以及不存在的页面,导致内存泄漏。如果来回切换更是会使定时器叠加,导致计数异常的问题。
为什么useEffect回调不能直接使用async
useEffect
的回调函数不能直接标记为async
,因为:async
函数会隐式返回一个Promise,而useEffect
期望其回调要么不返回任何内容,要么返回一个清理函数。如果返回Promise,React无法正确处理清理逻辑
正确做法是在回调内部使用async函数:
jsx
useEffect(() => {
const fetchData = async () => {
const result = await someApiCall();
// 处理结果
};
fetchData();
}, []);
二、组件生命周期的三个阶段使用useEffect
-
挂载阶段(Mounting)
该节点首先进行组件初始化 ,即创建组件实例,然后运行组件内的所有逻辑代码 ,最后生成虚拟DOM并渲染到实际DOM中。
当组渲染完后
useEffect
执行,这时我们可以使用useEffect
来处理挂载阶段的逻辑,比如发生获取数据的请求。jsxuseEffect(() => { // 挂载时执行的代码(仅执行一次) console.log('组件已挂载'); console.log('空依赖数组effect执行') return () => { // 清理函数(将在卸载时执行) } }, []) // 空依赖数组确保只在挂载时执行
-
更新阶段(Updating)
当组件的props或state发生变化时,组件会进入更新阶段,进入更新阶段后首先会执行组件函数体代码 ,之后会对根据新的状态或属性对组件重新渲染。
这时候可以使用
useEffect
监听特定状态变化:jsxuseEffect(() => { // 当count变化时执行(包括首次挂载) console.log(`Count值已更新为: ${count}`); return () => { // 清理函数(在下次effect执行前或组件卸载时执行) } }, [count]) // count作为依赖项
-
卸载阶段(Unmounting)
进入卸载阶段,组件从会DOM中移除 并执行清理操作,如清除定时器、取消网络请求、移除事件监听器等
使用
useEffect
的清理函数处理卸载逻辑:jsxuseEffect(() => { const timer = setInterval(() => { console.log('定时器运行中...'); }, 1000); return () => { clearInterval(timer); // 清除定时器 console.log('组件已卸载,定时器已清除'); } }, [])
三、具体作用
-
组件渲染后 :使用
useEffect
在组件挂载后请求数据,这样可以避免与渲染争抢资源,先完成初始渲染,再请求数据jsxuseEffect(() => { const loadData = async () => { const data = await fetch('/api/data'); // 更新状态 }; loadData(); }, []);
-
考虑竞态条件:在清理函数中取消未完成的请求
当用户在输入框快速输入"86"一词。输入"8"时发起请求A,输入"86"时发起请求B。**由于网络延迟,可能出现请求B比请求A先返回的情况,最终显示的反而是过时的"r"搜索结果。**这就会导致数据错误。
我们可以在发生新请求前,将旧请求标记为已经取消的状态来解决这一问题
jsxconst [query,setQuery] = useState('') const [isMounted,setIsMounted] = useState(true) useEffect(() => { setIsMounted(true) const loadData = async () => { const data = await fetch('/api/data'); if (isMounted) { // 更新状态 } }; loadData(); return () => { setIsMounted(false); }; }, [query]);
当发起请求A后再发起请求B,由于网络问题,A的useeffect堵塞在fetch这里,此时B请求已经完成,会调用effect返回的清理函数将
isMounted
设置为false,若A请求完成后由于isMounted
为false所以不能更新数据。还可以通过取消未完成的请求来解决这个问题:
jsxconst [query,setQuery] = useState('') useEffect(() => { const controller = new AbortController(); const loadData = async () => { const res = await fetch('/api/data',{ signal: controller.signal }); const data = await res.json() // 处理逻辑 }; loadData() return () => controller.abort(); // 取消未完成的请求 }, [query]);