最近在慢慢由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虽能解决数值更新的问题,却也无法解决闭包值的问题。









网友评论