一、前言
2023年第一篇文章它来了,记得在2022年的某段时间里,countDown倒计时组件 被谈论的话题有点多,当时一直想写,没成想一直拖到了今年,那么咱还是老样子?经典的开场白?
二、效果预览
三、实现思路
3.1、奇怪的行为
有没有小伙伴跟我一样,一看到这个组件,上来就想获取一下当前的时分秒信息,有的话在评论区里扣1,让我不孤单哈哈哈,代码如下:
let time = new Date();
console.log('当前天:', time.getDate());
console.log('当前小时:', time.getHours());
console.log('当前分钟:', time.getMinutes());
console.log('当前秒:', time.getSeconds());
当然,没有这个冲动的同学肯定以为作者就这?
3.2、组件结构
<!-- 省略html标准结构 -->
<div>时间:</div>
<div class="time"></div>
<script>
const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
let valueRef = 30 * 60 * 60 * 1000;
document.querySelector('.time').innerHTML = parseTime(valueRef).days + ':' + parseTime(valueRef).hours + ':' + parseTime(valueRef).minutes + ':' + parseTime(valueRef).seconds;
/***
@description 根据你传入的值,返回给你天数、时、分、秒
*/
function parseTime(time) {
const days = Math.floor(time / DAY)
const hours = Math.floor((time % DAY) / HOUR)
const minutes = Math.floor((time % HOUR) / MINUTE)
const seconds = Math.floor((time % MINUTE) / SECOND)
return {
total: time,
days,
hours,
minutes,
seconds
}
}
</script>
是的,您没看错,这个组件的结构就是这么简单,它更强调的是逻辑。
现在的效果是静态的,页面是下面这样:
3.3、让时间动起来
首先我们来想一下组件的逻辑:
- 点击开始按钮,页面上的时间在有规律的递减。
3.3.1、加按钮
<div>时间:</div>
<div class="time"></div>
<button onclick="start()">开始</button>
3.3.2、start函数逻辑
现在我们先来分析一下:
- 当前页面的数字:1:6:0:0
- 点击开始按钮,时间开始递减
- 为了实现递减,我们有3种思路,
1、使用setTimeout
2、使用setInterval
3、使用 window.requestAnimationFrame函数。函数用于手动执行浏览器的重绘,这里我们采用这种方式来实现递减逻辑,原因就是误差更小。至于为啥误差小,还请各位伙伴自行百度。
- 页面刷新的问题我们现在有方案了,那么就剩下2个问题了:
1、如何控制刷新频率,倒计时组件都归0了,此时页面就应该停止刷新
2、在刷新的过程中,如何让页面上的数字 1秒1秒的减下去
1、控制刷新频率
这里我们先来看一下,如果用定时器的话,代码应该是这样的(伪代码):
let count = 0;
let timer = null;
timer = setInterval(
() => {
// 大于某个值就清除定时器
if (count >= 5){
timer && clearInerval(timer);
return;
}
count++;
}
)
上面的逻辑是我们的固有逻辑(就是根据习惯、经验不用想就能写出来的大致逻辑),但往往固有逻辑会影响我们的创新性,下面我介绍一个新的做法:
1、我们在当前时刻A,是可以拿到未来某个时间点的时间戳B。他俩的具体关系为:
A + xx毫秒 == B
2、假设我们countDown组件的执行时间是10s,假设countDown组件刚刚归0的时间戳是C,那么A + 10 * 1000 == C,这个等式一定成立。那么此时有的小伙伴就不明白了,10s为啥要乘1000?因为时间戳的单位是毫秒。
3、还有一个结论需要知道:countDown执行了1s,那么我们现实生活中也同样过了1s。基于这个结论,我们就可以推导下面2个公式:(1)
Date.now() - A == countDown执行了多长时间 == 现实生活中刚刚过了多长时间(2)
C - Date.now() == countDown还剩下多少时间没执行
2、页面上的数字递减
在 上一节 控制刷新频率 中,我们能够获得countDown组件还剩下多少时间,我们只需要在适当的时机把这个剩下的时间更新上去就可以了。
3、start函数所有代码
代码如下:
let valueRef = 10 * 1000;
let remainRef = null; // 暂停时间
let statusRef = null; // 组件状态标识
let radRef = null; // requestAnimationFrame引用
let interval = 1000; // 倒计时渲染时间间隔(单位ms)
const getCurrentRemain = function (){
// 获取当前组件剩余多少时间
return Math.max(endTimeRef - Date.now(), 0);
}
function isSameTime(time1, time2, interval) {
return Math.floor(time1 / interval) === Math.floor(time2 / interval)
}
// 暂停
const pause = function() {
statusRef = "paused";
if (radRef) {
window.cancelAnimationFrame(radRef);
}
}
const nextRemain = function(nextValue) {
remainRef = nextValue;
document.querySelector('.time').innerHTML = parseTime(nextValue).days + ':' + parseTime(nextValue).hours + ':' + parseTime(nextValue).minutes + ':' + parseTime(nextValue).seconds;
if (nextValue === 0) {
pause();
}
}
function macroTick() {
radRef = window.requestAnimationFrame(
function (){
if (statusRef === "started") {
const remainRemain = getCurrentRemain()
if (
!isSameTime(remainRemain, remainRef, intervalRef) ||
remainRemain === 0
) {
nextRemain(remainRemain)
}
if (remainRef > 0) {
macroTick()
}
}
}
);
}
const start = function() {
if (statusRef !== "started") {
endTimeRef = Date.now() + (statusRef === "paused" ? remainRef : valueRef)
statusRef = "started"
macroTick()
}
}
3.4、整个组件的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>时间:</div>
<div class="time"></div>
<button onclick="start()">倒计时开始</button>
<button onclick="pause()">暂停</button>
<button onclick="reset()">重置</button>
<script>
const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
let statusRef = null;
let endTimeRef = 0;
// let valueRef = 30 * 60 * 60 * 1000;
let valueRef = 10 * 1000;
let remainRef = null;
let intervalRef = 1000;
let radRef = null;
document.querySelector('.time').innerHTML = parseTime(valueRef).days + ':' + parseTime(valueRef).hours + ':' + parseTime(valueRef).minutes + ':' + parseTime(valueRef).seconds;
function isSameTime(time1, time2, interval) {
return Math.floor(time1 / interval) === Math.floor(time2 / interval)
}
/***
* @description - 解析时间
*/
function parseTime(time) {
const days = Math.floor(time / DAY)
const hours = Math.floor((time % DAY) / HOUR)
const minutes = Math.floor((time % HOUR) / MINUTE)
const seconds = Math.floor((time % MINUTE) / SECOND)
const milliseconds = Math.floor(time % SECOND)
return {
total: time,
days,
hours,
minutes,
seconds,
milliseconds,
}
}
const getCurrentRemain = function (){
return Math.max(endTimeRef - Date.now(), 0);
}
// 暂停
const pause = function() {
statusRef = "paused";
if (radRef) {
window.cancelAnimationFrame(radRef);
}
}
// 停止
const stop = function() {
pause()
statusRef = "stopped"
}
// 开始
const start = function() {
if (statusRef !== "started") {
// If status is paused, set endTime to now() + remain.
// If status is stopped, set endTime to now() + initial value.
endTimeRef = Date.now() + (statusRef === "paused" ? remainRef : valueRef)
console.log('当前时间戳:', Date.now())
statusRef = "started"
macroTick()
}
}
// 自动开始
const autostart = function() {
start()
}
// 重置
const reset = function() {
stop()
autostart()
}
const nextRemain = function(nextValue) {
remainRef = nextValue;
// update()
document.querySelector('.time').innerHTML = parseTime(nextValue).days + ':' + parseTime(nextValue).hours + ':' + parseTime(nextValue).minutes + ':' + parseTime(nextValue).seconds;
// current is immutable,
// Use the currentRef value instead of the current
if (nextValue === 0) {
pause();
}
}
function macroTick() {
radRef = window.requestAnimationFrame(
// in case of call reset immediately after finish
function (){
if (statusRef === "started") {
const remainRemain = getCurrentRemain()
if (
!isSameTime(remainRemain, remainRef, intervalRef) ||
remainRemain === 0
) {
console.log('判断1');
nextRemain(remainRemain)
}
if (remainRef > 0) {
console.log('判断2');
macroTick()
}
}
}
);
}
</script>
</body>
</html>
四、最后
好啦,基本的倒计时组件到这里也就结束啦,倒计时组件的源码在这里,有兴趣的小伙伴也可以关注一下我的专栏,这个专栏里汇聚了市面上所有组件的基本实现(专栏每周更新)。那么下次再见啦