最近在慢慢由class component转用function component代替,写法清爽了很多,不过带来的问题也有点多。。。
问题
首先让我们使用function component实现一个倒计时功能
import React, {useEffect, useState} from 'react';
import { Button } from 'antd-mobile';
let ClearTimer = null;
const Test = ()=>{
const [seconds, setSeconds] = useState(10);
useEffect(()=>{
ClearTimer = setInterval(countDown, 1000);
return clearUp;
}, []);
const clearUp = ()=>{
clearInterval(ClearTimer);
};
const countDown = ()=>{
let num = seconds - 1;
if(num < 1){
clearInterval(ClearTimer);
}
setSeconds(num);
};
return (
<div>
<Button>剩余时间{seconds}s</Button>
</div>
)
};
export default Test;
代码很简单,利用setInterval
逐秒递减。实际跑起来后,会发现 根本没有递减!!!一直卡在9s不动。
原因
究其原因,还是seconds
取值的问题。countDown
是个闭包传入setInterval
,其中seconds
的值,在传入的时候就已经决定了,不会再取当前值,所以每次执行countDown
函数seconds
的值都是10。
解决办法
1. setState
传入函数参数
更改countDown
的写法,setSeconds
不直接传入数值,而是传入一个callback
const countDown = ()=>{
setSeconds((pre)=>{
console.log('pre = ', pre);
let num = pre - 1;
if(num <= 0){
clearInterval(ClearTimer);
}
return num;
});
};
callback
的参数,为当前的seconds
值,而不再是闭包里的值,此时计算结果已经是正确了。
2. 利用useEffect
及setTimer
实现(不推荐,容易影响其他逻辑)
首先修改useEffect
,让其依赖seconds
进行刷新,setInterval
替换成setTimeout
useEffect(()=>{
ClearTimer = setTimeout(countDown, 1000);
return clearUp;
}, [seconds]);
再简单改下countDown
,只要调用setSeconds
就行
const countDown = ()=>{
seconds > 0 && setSeconds(seconds - 1);
};
更改后,每次setSeconds
都会引起useEffect
函数的刷新动作,从而重新生成一个新Timer
,保证了闭包内seconds
的值始终是最新的
总结
由闭包引起的变量值没更新的问题,在class component
中比较少见,而在function component
中一不注意就会踏入陷阱。在需要根据当前state
值计算下一个state
时,尽量传入setState
的函数参数,使用其参数state
来进行计算,避免受其他环境的影响,比如闭包、别处的引用修改。
注意:使用useReducer
虽能解决数值更新的问题,却也无法解决闭包值的问题。