我们通过之前的《前端中的事件循环 eventloop 机制》中也能知道, 倒计时通常有 2 种,一种是固定差值的倒计时,例如我们抢金达人中每题的答题时间是 10s;还有一种是固定时间点的倒计时,比如凌晨 0 点开始抢购。但这两种实现的方式都差不多。 我们以抢金达人中的固定差值倒计时为例,来用几种方法实现这个倒计时。这种固定差值的倒计时,无所谓从什么时间点开始,我只需要有 10 秒钟的时间即可。 最开始,我们在倒计时的过程中,为了增加用户的紧张心理,加了一个小数位,最简单的倒计时就实现了,每 100ms 减去 0.1,当进度减为 0 时,则结束: 若倒计时要求不严格,则就可以这样按照每 100ms 执行一次。若比较严格的时,我们可以根据上次执行的时间,来校准下次的执行周期: 与 setTimeout 相比,requestAnimationFrame 最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是 60Hz,那么回调函数就每 16.7ms 被执行一次,如果刷新率是 75Hz,那么这个时间间隔就变成了 1000/75=13.3ms,换句话说就是,requestAnimationFrame 的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题 以上两种方案都存在着一个问题,若当前标签页不可见时,或者在移动端被置于后台时,计时器均会被挂起或者变慢,导致重新回来后的计时器出错。比如当用户正在答题倒计时的过程中,当用户把 app 收到后台,倒计时就会停止,重新回到 app 后,倒计时接着之前的继续执行,这样是不对的。 这里我就对之前使用的倒计时进行了改进,不再用计数器来计算当前剩余的时间,而是利用固定时间点的倒计时方式进行计算。每当要开始倒计时器之前,都先计算出结束时的时间戳是多少。然后每次渲染时就获取下当前时间戳与结束时间戳之间的差值。这样就即使是切换 tab,或者将 app 收到后台,都是没有影响的。 但如果用户要修改它的本地时间,那就没办法了。我们也只是从精准度上进行考虑,同时后端也加上对时间的校验,即使修改了前端时间,后端也是校验不通过的。 可以点击链接查看简单的 demo。 当然,这只是很简单的功能,我们可以再添加几个配置,再完善下这个倒计时组件: 这里我们定义组件接收的参数类型为: 一个完整的组件在在于组件的健壮性和扩展性,我们这里也对外提供几个参数,方便调用。 完整的代码可以访问 GitHub 仓库:react-countdown。 调用方式: 还有更多的 demo 可以链接查看:react-countdown 的样例。 倒计时在我们日常项目应用地非常广泛,这里也是总结了下倒计时的用法,并形成一个通用的组件,当然,其中还有很多的不足,依然还需要进一步的完善。setTimeout
和setInterval
的计时器并不是准确的,因为有其他任务的执行,会推迟定时器的执行。这对一些相对比较严格计时器来说,倒计时的时间越长,误差就会越大。1. 计数器 #
this.timer = setInterval(() => {
const progress = this.state.progress;
const _progress = (progress * 10 - 1) / 10;
this.setState({ progress: _progress });
if (_progress <= 0) {
this.setState({ status: 'finished' });
this.execute('onEnd');
this.stop();
}
}, 100);
let lastTime = Date.now();
let delay = 100;
let diff = 0; // 每次校验的误差
let progress = 10;
function countdown() {
progress = (progress * 10 - 1) / 10;
console.log(progress);
const now = Date.now();
diff = now - lastTime - delay;
lastTime = now;
console.log(`diff: ${diff}, 下次周期:${delay - diff}`);
if (progress > 0) {
setTimeout(() => {
countdown();
}, delay - diff); // 每次进行校准
}
}
setTimeout(() => {
countdown();
}, delay);
2. requestAnimationFrame #
3. 固定时间点的倒计时 #
const CountDown = () => {
const endTime = useMemo(() => Date.now() + 1000 * 10, []); // 持续10s的时间
const [leftTime, setLeftTime] = useState(0);
useEffect(() => {
const count = () => {
let now = Date.now();
const diff = endTime - now;
if (diff >= 0) {
setLeftTime((diff / 1000).toFixed(2));
requestAnimationFrame(count);
} else {
setLeftTime(0);
}
};
count();
}, [endTime]);
return <p>{leftTime}p>;
};
interface CountDownProps {
total?: number; // 倒计时的时间,单位毫秒
endTime?: number; // 结束的时间点,与 total 二选一,且优先级更高,单位毫秒
format: string | ((progress: number) => string); // 要展示的时间格式,可以是字符串或者函数
diff?: number; // 频率,单位毫秒ms
onStart?: () => void; // 开始时的回调
onStep?: (step: number) => void; // 每次更新时执行的回调
onEnd?: () => void; // 结束时的回调
}
<CountDown
total={10 * 1000}
format={(progress) => 'wenzi ' + progress}
diff={10}
onStep={(step) => console.log(step)}
onEnd={() => console.log('end')}
/>
4. 总结 #
版权属于:
加速器之家
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论