Day 29 - Vanilla JS Countdown Timer

前言

JS 30 是由加拿大的全端工程师 Wes Bos 免费提供的 JavaScript 简单应用课程,课程主打 No FrameworksNo CompilersNo LibrariesNo Boilerplate 在30天的30部教学影片里,建立30个JavaScript的有趣小东西。

另外,Wes Bos 也很无私地在 Github 上公开了所有 JS 30 课程的程序码,有兴趣的话可以去 fork 或下载。


本日目标

制作网页版的倒数计时器,透过点按网页上的按钮快速设定倒数计时器或是在输入框内输入要设定倒数的分钟数。

在这里顺便说明什麽是Vanilla JSVanilla JS是一个快速、轻量化、跨平台的 JavaScript 框架。


解析程序码

HTML 部分

.timer是倒数计时器的本体,以下又可分成两大部分,.timer__controls是其下设置倒数计时器的控制列,.display是其下用来展示剩余时间和倒数结束时间的容器。

此外,可以发现在每一个button元素上,都有设定data-time属性,这个属性用来帮我们快速地设置倒数计时器且单位是以秒计算。

#custom内部放置一个文字输入框,在使用者输入数字按下 enter 後,这个数字会被拿来设定倒数计时器并以分钟作为单位。

.display__time-left用来放置正在倒数的时间(分:秒)。
display__end-time用来放置倒数结束的时间。

<div class="timer">
    <div class="timer__controls">
      <button data-time="20" class="timer__button">20 Secs</button>
      <button data-time="300" class="timer__button">Work 5</button>
      <button data-time="900" class="timer__button">Quick 15</button>
      <button data-time="1200" class="timer__button">Snack 20</button>
      <button data-time="3600" class="timer__button">Lunch Break</button>
      <form name="customForm" id="custom">
        <input type="text" name="minutes" placeholder="Enter Minutes">
      </form>
    </div>
    <div class="display">
      <h1 class="display__time-left"></h1>
      <p class="display__end-time"></p>
    </div>
</div>

JS 部分

宣告变数countdown,待会用来指定成後面setInterval()的间隔代码(Interval ID)。

宣告常数timeDisplay取得用来放置倒数时间的元素。

let countdown;
const timeDisplay = document.querySelector('.display__time-left');

timer()里首先要取得现在的时间(Date.now()),要注意这边回传的是以毫秒为单位的timestamp

接着宣告then作为倒数计时结束的时间点,把要倒数的秒数乘上1000(换成毫秒)再加上现在的时间(毫秒)就完成了。

再接下来可以注意到在第五行,呼叫了displayTimeLeft()这个方法,这个方法用来帮我们把倒数中的时间放到.display__time-left还有网页的标题里面(方法的详细内容在更下面)。

那为什麽在开始倒数之前,要先呼叫displayTimeLeft()呢? 因为用setInterval()倒数的话,它会有1秒的延迟时间,也就是说最一开始的那1秒是什麽都没有的状态,所以要呼叫displayTimeLeft()补上那1秒的空窗期。

然後就是用setInterval()开始倒数的部分啦~ 在这边宣告常数secondsLeft放入经计算後的剩余秒数,记得要除上1000,因为每一秒 then - Date.now() 的结果单位都是毫秒,最後用Math.round()四舍五入取最近的整数。

secondLeft小於0的时候,我们应该要立即停止倒数,这时候前面指定的 Interval ID 就派上用场啦~ 用clearInterval(countdown)就可以把倒数轻松移除。

function timer(seconds){
    const now = Date.now();
    const then = now +seconds * 1000;
    //run immediately not run after 1 sec
    displayTimeLeft(seconds);

    countdown = setInterval(()=>{
        const secondsLeft = Math.round((then - Date.now())/1000);
        //display it
        if(secondsLeft < 0){
            clearInterval(countdown);
            return;
        }
        displayTimeLeft(secondsLeft);
    },1000)
}

displayTimeLeft(),它会把拿到的剩余秒数换算成分和秒,再放回到'.display__time-left'里并同时修改网页标题。

这边要特别注意如果经换算後,秒数的部分不足10秒,则要透过条件判断在前方补上0。(ex. 10:5 >>> 10:05)

function displayTimeLeft(seconds){
    const minutes = Math.floor(seconds/60);
    const reminderSeconds = seconds%60;
    const display = `${minutes}:${reminderSeconds < 10 ? '0' : ''}${reminderSeconds}`;
    document.title = display;
    timeDisplay.textContent = display;
}

宣告常数endTime取得放置倒数结束时间的元素,接下来就要用displayEndTime()来处理倒数结束的时间罗!

displayEndTime(),他把传入的timestamp(单位 : 毫秒),用new Date(timestamp)建立成一个Date物件再放入常数end里面。

接下来 Do Re Mi SO,把endTime(结束时间),修改成自Date物件取得的小时、分钟。特别注意遇到分钟数不足10分钟时,要利用条件判断在数字前方补上0,例如: 21:5 >>> 21:05。

const endTime = document.querySelector('.display__end-time');
/*中略....*/
function displayEndTime(timestamp){
    const end = new Date(timestamp);
    const hour = end.getHours();
    const minutes = end.getMinutes();
    endTime.textContent = `Be Back At ${hour}:${minutes < 10 ? '0' : ''}${minutes}`;
}

new Date(timestamp)的一些操作如下图 :

(我一开始觉得怪怪的,为什麽getMonth()回传的是8而不是9? 後来一查才发现0代表的是1月 XD)

下面把timer()倒数结束的时间(then,毫秒),传入displayEndTime(),修改页面上的倒数结束时间。

function timer(seconds){
    /*上略...*/
    displayEndTime(then);
    /*下略...*/
}

宣告常数buttons取得所有用来快速设定倒数计时器的按钮。

然後为每个button都注册click event listenerstartTimer()作为event handler

startTimer(),宣告常数seconds取得被点击按钮上的data-time属性值,接着把这个seconds丢入timer(),成功建立一个倒数计时器。

const buttons = document.querySelectorAll('[data-time]');
/*中略...*/
function startTimer(){
    const seconds = parseInt(this.dataset.time);
    timer(seconds);
}

buttons.forEach(button => button.addEventListener('click',startTimer));

如果这时候开始疯狂点击按钮建立倒数计时器,你会发现网页上的倒数时间开始"抽搐",因为之前建立的倒数计时器仍在运作,造成网页倒数时间的文字被频繁地修改,然後不自然的"抽搐"就出现了。

所以我们需要在timer()的最上面加上clearInterval(countdown);这一行,在每一次设定倒数计时器的时候,把之前的倒数计时器通通清掉。

function timer(seconds){
    //clear any existing timer
    clearInterval(countdown);
    /*下略...*/
}

最後要来处理文字输入框的部分,在form元素上注册submit event listener以後面的function()作为event handler

因为submit表单时会重新整理网页,所以在第一行写e.preventDefault()防止网页重新整理。接着宣告常数mins承接来自<input name="minutes">的分钟数(value),然後再把分钟数乘上60换成秒传入timer(),成功设定倒数计时器。最後一行的this.reset()用来重置表单元素。

document.customForm.addEventListener('submit',function(e){
    e.preventDefault();
    const mins = this.minutes.value;
    console.log(mins);
    timer(mins * 60);
    this.reset();
});
补充资料:

Date.now()
WindowOrWorkerGlobalScope.setInterval()
clearInterval()
Math.floor()
Node.textContent
Date.prototype.getHours()
Date.prototype.getMinutes()
HTMLElement.dataset

范例网页

完整程序码请点此


<<:  多工的陷阱

>>:  [Day 20] 实作 - 介面篇4

Computed vs Methods

昨天已经把JSON档建置好了!今天就可以取用JSON档的资料然後实作出Methods和Compute...

Day 24 Encapsulation

物件导向程序设计有三大特性:封装、继承、多型,今天要来介绍封装。 封装顾名思义就是把属性封在类别里面...

【Day 09】Hook 的奇妙冒险 - Ring3 Hook

环境 Windows 10 21H1 Visual Studio 2019 x64dbg Aug 2...

Day 17 - [语料库模型] 05-实体对应

在语句中常会出现概念相似的词,包括某类物品、地名、时间...等。例如,轮椅、拐杖、助行器、电动床都属...

[DAY 28] _看门狗简介_视窗看门狗(2)

昨天主要介绍了视窗看门狗和独立看门狗的差别,今天来看这如何计算,这计算方式再参考手册里面有举例说明,...