背景
useEffect
看似很简单,其实不然。由于没有理解 useEffect
的工作原理,我被狠狠坑了一回。
事情是这样的:我想实现一个很简单的“物体下落”效果:
- 物体的垂直位置用
positionY
这个 state 来表示,从上往下计算,positionY
越大就离屏幕顶端越远; - 设置一个
setInterval
,每隔一小段时间(30 ms)就增加positionY
,然后基于positionY
渲染物体,从而实现“物体下落”的效果;
看似非常简单,但遇到了意想不到的困难!
失败的尝试
我想,既然 setInterval
中的函数每隔一段时间就会触发,那么 setInterval
显然只需要运行一次就可以了!
众所周知,只运行一次函数的方式就是使用第二个参数为空数组的 useEffect
钩子。于是我写出了下面这个版本的代码:
function App() {
const [positionY, setPositionY] = useState(0)
useEffect(() => {
timer = setInterval(() => {
// 落到屏幕下方时停止下落
if (positionY < WINDOW_HEIGHT) {
setPositionY(positionY + 5)
}
}, 30)
return () => {
clearInterval(timer);
};
}, []);
return (
// 略
}
结果一运行我就傻了:物体根本没在动!准确地说,只是微微动了一下,之后就一直静止不懂了
哪里出错了?
为什么上面那样写会失败?后来终于找到了原因:
- 第一次运行
App()
时,useEffect
被运行,设置了setInterval
,并且setInterval
也的确触发了setPositionY(positionY + 5)
,因此物体才会有最开始的“微微一动”; - 但当
positionY
更新之后,React 会用更新之后的 state 重新调用App()
,但这次useEffect
中的函数就不会运行了;
所导致的结果就是:setInterval
中的函数拿到的始终都是 positionY
的初始值。
为什么会这样?因为 setInterval
是在第一次运行 App()
时被创建的,那个 App()
里的 positionY
从未改变。
更新之后的 positionY
的确被用来第二次运行 App()
了,但第二次运行 App()
又不会运行 setInterval
了。
真是意想不到啊!
正确写法
至少有两种正确写法,示例如下。
方法一
第一种方法是:
- 在每次
positionY
改变时都执行useEffect
,使用最新的positionY
来创建一个新的setInterval
; - 与此同时,之前的
setInterval
会被自动清除,同时只会有一个setInterval
计时器;
示例如下:
function App() {
const [positionY, setPositionY] = useState(0)
useEffect(() => {
timer = setInterval(() => {
if (positionY < WINDOW_HEIGHT) {
setPositionY(positionY + 5)
}
}, 30)
return () => {
// 每次执行新的 useEffect 时,之前的 useEffect 会被清理
// 这个函数会被调用,从而清除之前的 setInterval 计时器
clearInterval(timer);
};
}, [positionY]); // 这样保证了每次 positionY 改变时,useEffect 都会执行
return (
// 略
}
方法二
第二个方法是:
setState
可以接受一个函数,该函数的第一个参数即是最新的 state。通过这种方法可以确保拿到最新的 state;
示例如下:
function App() {
const [positionY, setPositionY] = useState(0)
useEffect(() => {
timer = setInterval(() => {
// 这样能保证拿到最新的 positionY
setPositionY(oldVal => {
if (oldVal < WINDOW_HEIGHT) {
return oldVal + 5
}
return oldVal
})
}, 30)
return () => {
clearInterval(timer);
};
}, []); // 只需要执行一次 useEffect 即可
return (
// 略
}