前言
本次来分享一个在基于react18中是实现一个"按钮触发倒计时"的完整思路和优化实践。
使用定时器的弊端
setInterval
或者 setTimeout
定时器实现倒计时是一个比较常方案,但是有一个无法避免的 "坑",在浏览器中,如果用户切换到其它的tab页签后,定时器就会不准确
使用 rAF 实现
requestAnimationFrame有以下几个好处:
- rAF 与浏览器渲染节奏同步,动画/计时更平滑
- rAF 提供每帧时间戳,易做精度与边界控制
核心思路(Hooks + rAF)
-
使用
useRef
存放"可变但不触发渲染"的数据rafIdRef
:当前动画帧 idendTimeRef
:结束时间戳(毫秒)lastSecondRef
:上一次已渲染的秒数
-
仅在秒数变化时
setState
(用lastSecondRef
去重),因为 rAF 每秒可触发 60 次渲染,过多占用cpu -
启动新倒计时前,取消旧的 rAF,避免并发
-
组件卸载时取消 rAF,防泄漏
-
用
Math.ceil(diff / 1000)
处理边界更稳。Math.ceil
在接近 0 的边界,ceil 更符合"剩余秒数"的用户心智,不会提前跳到更小的值
核心代码
scss
import React, { useRef, useState, useEffect } from 'react'
function App() {
const [count, setCount] = useState(5)
const rafIdRef = useRef(null)
const endTimeRef = useRef(0)
const lastSecondRef = useRef(null)
const startCountdown = (durationSec) => {
// 1) 防并发:先取消已有动画帧
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current)
rafIdRef.current = null
}
// 2) 初始化时间数据
const start = performance.now()
endTimeRef.current = start + durationSec * 1000
lastSecondRef.current = null
// 3) rAF 驱动:只在秒数变化时 setState
const step = (ts) => {
const diff = endTimeRef.current - ts
if (diff > 0) {
const sec = Math.ceil(diff / 1000)
// 关键优化,避免重复过渡的 setState
if (sec !== lastSecondRef.current) {
lastSecondRef.current = sec
setCount(sec)
}
rafIdRef.current = requestAnimationFrame(step)
} else {
setCount(0)
rafIdRef.current = null
}
}
rafIdRef.current = requestAnimationFrame(step)
}
// 4) 卸载清理,防止泄漏
useEffect(() => {
return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current)
}
}
}, [])
return (
<div>
<p>剩余秒数:{count}</p>
<button onClick={() => startCountdown(5)}>开始 5 秒倒计时</button>
</div>
)
}
export default App
结语
如果这篇文章帮到了你,欢迎点赞👍和关注⭐️。
文章如有错误之处,希望在评论区指正🙏🙏。