setTimeout函数中执行代码的时间肯定是要大于等于setTimeout时间的,那么就可能出现设定的 1 秒,实际执行却执行了 2 秒的情况,那么我们的实现思路也很简单,每次计算一下setTimeout实际执行的时间,然后动态的调整下一次执行的时间,而不是设置固定的值。
我们来用图表举例推演一下每次执行的情况:
从中可以看到:下次执行的时间 nextTime = 1000 - totleTime % 1000;这样我们就可以得出下次执行的时间,从而每次都去动态的调整多余消耗的时间,大大减小倒计时最终的误差
还有需要考虑的是,实际业务中返回的剩余时间肯定不会是整数,所以我们的第一次执行的时间最好可以先让剩余时间变为整数,这样可以在倒计时到最后一秒时更加的精确。
根据上述的思路来看一下最终封装出来的 react hooks:
const useCountDown = ({ leftTime, ms = 1000, onEnd }) => {
const countdownTimer = useRef();
const startTimer = useRef();
//记录初始时间
const startTimeRef = useRef(performance.now());
// 第一次执行的时间处理,让下一次倒计时时调整为整数
const nextTimeRef = useRef(leftTime % ms);
const [count, setCount] = useState(leftTime);
const clearTimer = () => {
countdownTimer.current && clearTimeout(countdownTimer.current);
startTimer.current && clearTimeout(startTimer.current);
};
const startCountDown = () => {
clearTimer();
const currentTime = performance.now();
// 算出每次实际执行的时间
const executionTime = currentTime - startTimeRef.current;
// 实际执行时间大于上一次需要执行的时间,说明执行时间多了,否则需要补上差的时间
const diffTime =
executionTime > nextTimeRef.current
? executionTime - nextTimeRef.current
: nextTimeRef.current - executionTime;
setCount((count) => {
const nextCount =
count - (Math.floor(executionTime / ms) || 1) * ms - nt;
return nextCount <= 0 ? 0 : nextCount;
});
// 算出下一次的时间
nextTimeRef.current =
executionTime > nextTimeRef.current ? ms - diffTime : ms + diffTime;
// 重置初始时间
startTimeRef.current = performance.now();
countdownTimer.current = setTimeout(() => {
requestAnimationFrame(startCountDown);
}, nextTimeRef.current);
};
useEffect(() => {
setCount(leftTime);
startTimer.current = setTimeout(startCountDown, nextTimeRef.current);
return () => {
clearTimer();
};
}, [leftTime]);
useEffect(() => {
if (count <= 0) {
clearTimer();
onEnd && onEnd();
}
}, [count]);
return count;
};
export default useCountDown;