function example1(leftTime) { let t = leftTime; setInterval(() => { t = t - 1000; console.log(t); }, 1000); } // 堆代码 duidaima.com example1(10);可以看到使用 setInterval 即可,但是 setInterval 真的准确吗?我们来看一下 MDN 中的说明:
如果你的代码逻辑执行时间可能比定时器时间间隔要长,建议你使用递归调用了 [setTimeout()](https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout) 的具名函数。例如,使用 setInterval() 以 5 秒的间隔轮询服务器,可能因网络延迟、服务器无响应以及许多其他的问题而导致请求无法在分配的时间内完成。
function example2(leftTime) { let t = leftTime; setTimeout(() => { t = t - 1000; if (t > 0) { console.log(t); example2(t); } console.log(t); }, 1000); }MDN 中也说了,有很多因素会导致 setTimeout 的回调函数执行比设定的预期值更久,比如嵌套超时、非活动标签超时、追踪型脚本的节流、超时延迟等等,详情见https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout,总就就是和 setInterval 差不多,时间一长,就会有误差出现,而且 setTimeout 有一个很不好的点在于,当你的程序在后台运行时,setTimeout 也会一直执行,这样会严重的而浪费性能,那么有什么办法可以解决这种问题吗?
function example4(leftTime) { let t = leftTime; function start() { requestAnimationFrame(() => { t = t - 1000; setTimeout(() => { console.log(t); start(); }, 1000); }); } start(); }为什么要使用 requestAnimationFrame + setTimeout 呢?一个是息屏或者切后台的操作时,requestAnimationFrame 是不会继续调用函数的,但是如果只使用 requestAnimationFrame 的话,函数相当于 1 秒的时候要调用 60 次,太浪费性能。在切后台或者息屏的实际执行时会发现,当回到页面时,倒计时会接着切后台时的时间执行,而没有更新到最新的时间,这样的 bug 是接受不了的。
function example5(leftTime) { const now = performace.now(); function start() { setTimeout(() => { const diff = leftTime - (performace.now() - now); console.log(diff); requestAnimationFrame(start); }, 1000); } start(); }上面的代码实现思路其实在实际的业务中已经能够满足我们的使用场景,但其实还是没有解决 setTimeout 会延迟的问题,当线程被占用之后,很容易出现误差,那么有什么更新的办法进行处理呢?
0 0 1000 0 1 1200 800 1200 2 1100 700 2300 3 1000 700 3300 4 2200 500 5500 5 1300 200 6800 6 1200 1000 8000 … … … …从中可以看到:下次执行的时间 nextTime = 1000 - totleTime % 1000;这样我们就可以得出下次执行的时间,从而每次都去动态的调整多余消耗的时间,大大减小倒计时最终的误差。还有需要考虑的是,实际业务中返回的剩余时间肯定不会是整数,所以我们的第一次执行的时间最好可以先让剩余时间变为整数,这样可以在倒计时到最后一秒时更加的精确。
const useCountDown = ({ leftTime, ms = 1000, onEnd }: CountDownProps) => { const countdownTimer = useRef<NodeJS.Timeout | null>(); const startTimeRef = useRef<number>(performance.now()); const nextTimeRef = useRef<number>(leftTime % ms); const totalTimeRef = useRef<number>(0); const [count, setCount] = useState(leftTime); const preLeftTime = usePrevious(leftTime); const clearTimer = useCallback(() => { if (countdownTimer.current) { clearTimeout(countdownTimer.current); countdownTimer.current = null; } }, []); // requestAnimationFrame const startCountDown = useCallback( (nt: number = 0) => { clearTimer(); // 堆代码 duidaima.com // 每次实际执行的时间 const executionTime = performance.now() - startTimeRef.current; // 1.x totalTimeRef.current = totalTimeRef.current + executionTime; // 剩余时间减去应该执行的时间 setCount((count) => { const nextCount = count - (Math.floor(executionTime / ms) || 1) * ms - nt; return nextCount <= 0 ? 0 : nextCount; }); // 算出下一次的时间 nextTimeRef.current = ms - (totalTimeRef.current % ms); // 重置初始时间 startTimeRef.current = performance.now(); countdownTimer.current = setTimeout(() => { requestAnimationFrame(() => startCountDown(0)); }, nextTimeRef.current); }, [ms] ); useEffect(() => { if (preLeftTime !== leftTime && preLeftTime !== undefined) { clearTimer(); setCount(() => leftTime); nextTimeRef.current = leftTime % ms; countdownTimer.current = setTimeout(() => { requestAnimationFrame(() => startCountDown(nextTimeRef.current) ); }, nextTimeRef.current); } }, [leftTime, ms]); useEffect(() => { countdownTimer.current = setTimeout( () => startCountDown(nextTimeRef.current), nextTimeRef.current ); return () => { clearTimer(); }; }, []); useEffect(() => { if (count <= 0) { clearTimer(); onEnd && onEnd(); } }, [count]); const formatCount = parseMillisecond(count); return { formatCount, count }; }; export default useCountDown;如果想要封装组件的话,可以在 hooks 的基础上进行二次封装。