手撸一个Countdown倒计时组件

560 阅读4分钟

一、前言

2023年第一篇文章它来了,记得在2022年的某段时间里,countDown倒计时组件 被谈论的话题有点多,当时一直想写,没成想一直拖到了今年,那么咱还是老样子?经典的开场白?

干就完了.jpg

二、效果预览

倒计时组件1.gif

三、实现思路

3.1、奇怪的行为

有没有小伙伴跟我一样,一看到这个组件,上来就想获取一下当前的时分秒信息,有的话在评论区里扣1,让我不孤单哈哈哈,代码如下:

let time = new Date();
console.log('当前天:', time.getDate());
console.log('当前小时:', time.getHours());
console.log('当前分钟:', time.getMinutes());
console.log('当前秒:', time.getSeconds());

当然,没有这个冲动的同学肯定以为作者就这?

来点干货.png

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>

是的,您没看错,这个组件的结构就是这么简单,它更强调的是逻辑

现在的效果是静态的,页面是下面这样:

倒计时组件1.png

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>

四、最后

好啦,基本的倒计时组件到这里也就结束啦,倒计时组件的源码在这里,有兴趣的小伙伴也可以关注一下我的专栏,这个专栏里汇聚了市面上所有组件的基本实现(专栏每周更新)。那么下次再见啦