起因
简简单单,我想实现一个倒计时组件,如下的功能。对比学习 Class 组件和 React Hook 实现方式的区别,不做不知道,一做发现了大玄机。且向下看。
React Hook
关于 React Hook 快速入门,请看这篇博客:juejin.cn/post/692715…
递增的变量
如果想实现一个 1 秒增加 1 的变量,使用 React Hook,我们很容易写出下面有问题的代码:
const Demo = ()=>{
const [count, setCount] = useState(0);
console.log("init...");
useEffect(()=>{
const timer = setInterval(() => {
console.log("interval...");
setCount(count+1);
}, 1000);
return ()=>{
console.log("clear...");
clearTimeout(timer);
}
}, []);
return (
<p>{count}</p>
)
}
useEffect(()=>{}, [])
表示的是组件加载时会执行的逻辑,[]
空数组表示 useEffect
里面的代码逻辑跟组件中的变量无关。
为了说明代码的执行顺序,添加了 console.log 的提示信息,会发现 init 只会输出一次,interval 每秒输出一次。
事实上,这么写是有问题的。setInterval 是会每秒钟执行一次,但是呢? count 的值永远是第一次获取的值也就是 0,也就是每秒中 setInterval 中执行的逻辑是 setCount(1)
。所以页面上 count 的值从 0 变成 1 就没有变过了。在 React Hook (juejin.cn/post/692715…) 快速入门也提到了,使用 setState
的变量是快照 snap 时的值。
另一方面,使用 useEffect
有一个重要原则就是对于依赖的变量要 诚实,可以看到 setInterval
的逻辑中明明依赖了 count 的值,也就是这次更新的 count 要依赖上一次 count 的值,+1 完成。想到这一点,我们可以优化一下:
const Demo = ()=>{
const [count, setCount] = useState(0);
console.log("init...");
useEffect(()=>{
const timer = setInterval(() => {
console.log("interval...");
setCount(count+1);
}, 1000);
return ()=>{
console.log("clear...");
clearTimeout(timer);
}
}, [count]);
return (
<p>{count}</p>
)
}
这样效果是对的,但是性能不好。每当 count 更改了, useEffect 就会渲染一次,定时器也会不停的被新增与移除。
控制台在不停的 init、clear、interval 循环输出。站在神坛的 React Hook 本质也是一个函数而已。当 setState
的变量 count 变化的时候,Demo 才会重新执行一遍。那么执行两次 Demo 函数是两个独立的函数,没有什么牵扯。
这篇博客 www.fly63.com/article/det… 介绍了 4 种优雅的解决方案。这里详细解释一下用 useRef 的实现方式:
function Counter() {
let [count, setCount] = useState(0);
const myRef = useRef(null);
myRef.current = () => {
setCount(count + 1);
};
useEffect(() => {
let id = setInterval(()=>{
myRef.current();
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
在 myRef.current(); 的外层要包一层函数,为什么呢?道理同下:
let cur = ()=>{
console.log(1);
}
setInterval(cur, 1000);
cur = ()=>{
console.log(2);
}
循环输出 1, 而不是最新的 2
let cur = ()=>{
console.log(1);
}
setInterval(()=>{
cur();
}, 1000);
cur = ()=>{
console.log(2);
}
这样循环输出 2.
倒计时
很抱歉,倒计时组件最终版本并没有用到上面的技巧使用 myRef.current 去获取最新的数据。而是根据当前时间跟倒计时的时间做比较,这样做可以保证精度,我们都知道 setInterval setTimeout
是宏任务,并不能保证函数执行的精度,所以将时间计算放在了函数中,函数执行的时候获取当前的时间然后做差,保证了倒计时的可靠。
let timer = null;
const CountDownHook = (props) => {
const finishDate = props.finishDate || "2021/02/17 23:47:00";
const [second, setSecond] = useState(0);
const [minute, setMinute] = useState(0);
const [hour, setHour] = useState(0);
const [day, setDay] = useState(0);
const calulate = ()=>{
const seconds = new Date(finishDate).getTime() - new Date().getTime();
if(seconds<=0){
clearInterval(timer);
return;
}
const days = Math.floor(seconds / (24 * 60 * 60 * 1000));
const hours = Math.floor((seconds - days * 24 * 60 * 60 * 1000) / (60 * 60 * 1000));
const minutes = Math.floor((seconds - days * 24 * 60 * 60 * 1000 - hours * 60 * 60 * 1000) / (60 * 1000));
const lastSecond = Math.floor((seconds - days * 24 * 60 * 60 * 1000 - hours * 60 * 60 * 1000 - minutes * 60 * 1000) / 1000);
setSecond(lastSecond);
setMinute(minutes);
setHour(hours);
setDay(days);
}
useEffect(() => {
// 初始化
calulate();
// 每秒执行
timer = setInterval(calulate, 1000)
return () => {
clearTimeout(timer);
}
}, [])
return ( <p style = {{marginLeft: "100px",marginTop: "100px"}} > 距离 {finishDate}倒计时: {day} 天 {hour} 小时 {minute} 分钟 {second} 秒 </p>)
}
Class 版本
class CountDownClass extends React.Component {
constructor(props) {
super(props);
this.timer = null;
this.state = {
day: 0,
hour: 0,
minute: 0,
second: 0
}
}
changeSecond = () => {
const finishDate = this.props.finishDate || "2021/02/17 20:00:00";
const seconds = new Date(finishDate).getTime() - new Date().getTime();
if(seconds<=0){
clearInterval(this.timer);
return;
}
const days = Math.floor(seconds / (24 * 60 * 60 * 1000));
const hours = Math.floor((seconds - days * 24 * 60 * 60 * 1000) / (60 * 60 * 1000));
const minutes = Math.floor((seconds - days * 24 * 60 * 60 * 1000 - hours * 60 * 60 * 1000) / (60 * 1000));
const lastSecond = Math.floor((seconds - days * 24 * 60 * 60 * 1000 - hours * 60 * 60 * 1000 - minutes * 60 * 1000) / 1000);
this.setState({
day: days,
hour: hours,
minute: minutes,
second: lastSecond
});
}
componentDidMount() {
// 初始化
this.changeSecond();
// 每秒执行
this.timer = setInterval(this.changeSecond, 1000);
}
componentWillUnmount(){
clearInterval(this.timer);
}
render() {
const finishDate = this.props.finishDate || "2021/02/17 23:00:00";
const {day, hour, minute, second} = this.state;
return (
<p style = {{marginLeft: "100px",marginTop: "10px"}} > 距离 {finishDate}倒计时: {day} 天 {hour} 小时 {minute} 分钟 {second} 秒 </p>
)
}
}