大家好,我是你们的老朋友FogLetter,今天我们来聊聊React Hooks中那个既让人爱又让人恨的useEffect。
一、从"副作用"说起
在React的世界里,我们总是追求"纯粹"的组件------给定相同的props,永远返回相同的JSX。但现实是骨感的,我们需要处理数据获取、订阅、手动修改DOM等"脏活累活"。这些操作就被称为"副作用"(side effects)。
useEffect就是React给我们的一把瑞士军刀,专门用来处理这些副作用。它就像是组件的"后花园",在这里我们可以做那些在主渲染流程中不方便做的事情。
javascript
useEffect(() => {
// 这里是你的"后花园",可以尽情搞事情
});
二、useEffect的基本用法
1. 组件挂载时执行
想象一下,你刚搬进新家(组件挂载),第一件事是什么?可能是开窗通风、打开WiFi,或者像我一样先点个外卖。在React中,这些"搬进新家后要做的事"就可以放在useEffect里:
javascript
useEffect(() => {
console.log('欢迎来到新家!');
// 这里可以放初始化逻辑
}, []); // 注意这个空数组
那个空数组[]
就像是你的购房合同,告诉React:"这个效果只在我搬进来时执行一次"。
2. 依赖更新时执行
有时候,我们希望在特定状态变化时执行一些操作。比如你家猫主子体重变化时,需要调整喂食量:
javascript
const [catWeight, setCatWeight] = useState(5);
useEffect(() => {
if (catWeight > 8) {
console.log('主子该减肥了,减少罐头供应!');
} else if (catWeight < 6) {
console.log('主子瘦了,加餐!');
}
}, [catWeight]); // 只在catWeight变化时执行
3. 组件卸载时清理
搬走时(组件卸载)也得收拾屋子,否则可能会留下"内存泄漏"的烂摊子。比如取消订阅、清除定时器等:
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器在运行...');
}, 1000);
return () => {
clearInterval(timer); // 搬走前记得关掉定时器
console.log('已收拾干净,不留下一片云彩');
};
}, []);
三、useEffect的进阶玩法
1. 数据获取的艺术
在组件中获取数据是个常见需求,但也是个容易踩坑的地方。看看这个例子:
javascript
useEffect(() => {
let isMounted = true; // 标记组件是否还挂着
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
if (isMounted) {
setData(data); // 只有组件还挂着才更新状态
}
};
fetchData();
return () => {
isMounted = false; // 组件卸载时标记为false
};
}, []);
这个模式避免了组件卸载后仍然设置状态的警告,就像你网购了东西但搬家了,得告诉快递员"别送了,我搬走了"。
2. 为什么useEffect不能直接async?
很多新手会这样写:
javascript
// 错误示范!
useEffect(async () => {
const data = await fetchData();
setData(data);
}, []);
这样写会报错,因为async函数会隐式返回Promise,而useEffect期望它的第一个参数要么不返回任何东西,要么返回一个清理函数。正确的打开方式是:
javascript
useEffect(() => {
const fetchData = async () => {
const data = await fetchData();
setData(data);
};
fetchData();
}, []);
3. 性能优化:依赖项的艺术
依赖项数组是useEffect的精髓所在,但也容易让人头疼。记住这个黄金法则:
- 空数组
[]
:只在挂载时运行 - 不传数组:每次渲染后都运行
- 有依赖项的数组
[a, b]
:当a或b变化时运行
但有时候依赖项会"说谎":
javascript
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远打印0!
}, 1000);
return () => clearInterval(timer);
}, []); // 故意不把count放进依赖项
这种情况下,count的值会被"锁定"在初始值0,因为effect只在挂载时运行一次,它"记住"的是当时的count值。解决方法要么把count加入依赖项,要么使用函数式更新:
javascript
setCount(prevCount => prevCount + 1); // 这样就不需要把count加入依赖项
四、useEffect与生命周期的关系
对于从class组件转过来的同学,可能会好奇useEffect和生命周期方法的对应关系:
componentDidMount
:useEffect(fn, [])
componentDidUpdate
:useEffect(fn)
或useEffect(fn, [a, b])
componentWillUnmount
:useEffect(() => { return fn }, [])
但要注意,useEffect的思维模型和生命周期方法有本质不同。React团队希望我们以"同步副作用到状态"的方式思考,而不是"在某个生命周期执行操作"。
五、常见坑与最佳实践
-
无限循环陷阱:
javascriptconst [data, setData] = useState(null); useEffect(() => { fetchData().then(data => setData(data)); }, [data]); // 哎呀,setData会触发重新渲染,然后data变化又触发effect...
-
遗忘清理函数: 不清理订阅、定时器、事件监听器等会导致内存泄漏,就像离开酒店不关水龙头。
-
依赖项地狱: 当effect依赖太多状态时,考虑:
- 是否可以把相关逻辑拆分成更小的effect
- 是否可以把一些逻辑移到事件处理函数中
-
最佳实践:
- 每个effect只做一件事(单一职责原则)
- 把不依赖props和state的代码移到effect外部
- 优先使用函数式更新(如
setCount(c => c + 1)
)
六、实战:一个完整的例子
让我们用Timer组件来总结今天的内容:
javascript
import { useState, useEffect } from 'react';
const Timer = () => {
const [time, setTime] = useState(0);
useEffect(() => {
console.log('Timer挂载了');
const timer = setInterval(() => {
setTime(prevTime => prevTime + 1); // 使用函数式更新避免依赖time
}, 1000);
return () => {
console.log('Timer即将卸载');
clearInterval(timer); // 必须清理!
};
}, []); // 空数组表示只在挂载时执行
return <div>已经运行{time}秒</div>;
};
这个简单的计时器展示了useEffect的典型用法:
- 在挂载时启动计时器
- 在卸载时清理计时器
- 使用函数式更新避免依赖项问题
七、总结
useEffect是React Hooks中最强大但也最具挑战性的Hook之一。它让我们能够:
- 在组件渲染后执行副作用
- 根据依赖项变化有条件地执行代码
- 在组件卸载时进行清理
记住这些要点:
- 副作用:处理数据获取、订阅、DOM操作等"非纯"操作
- 依赖项:精确控制effect的执行时机
- 清理:防止内存泄漏的关键步骤
- 异步:在effect内部定义async函数,而不是直接把effect变成async
useEffect就像是你组件的"生活管家",帮你处理各种杂务,但要用好它,需要理解它的工作方式和最佳实践。
最后送大家一句话:"With great power comes great responsibility." ------ 能力越大,责任越大。useEffect给了我们很大的能力,但也要求我们更负责任地管理副作用。
希望这篇笔记对你有帮助!如果觉得不错,别忘了点赞收藏~我们下期再见!