1
\$\begingroup\$

This is my first JavaScript application (it's inspired by this timer). How could I improve it?

I had to set the setInterval() to 980ms instead of 1000ms because it was slower than a real clock for some reason.

The full app

Main JS:

var btn1 = document.getElementById('start');
var btn2 = document.getElementById('pause');
var btn3 = document.getElementById('restart')

var h1 = document.getElementById('h1');
var h1Clr = h1.style.color;
var h1Nmb = 0;

var h2 = document.getElementById('h2');
var h2Clr = h1.style.color;
var h2Nmb = 0;

var m1 = document.getElementById('m1');
var m1Clr = h1.style.color;
var m1Nmb = 0;

var m2 = document.getElementById('m2');
var m2Clr = h1.style.color;
var m2Nmb = 0;

var s1 = document.getElementById('s1');
var s1Clr = h1.style.color;
var s1Nmb = 0;

var s2 = document.getElementById('s2');
var s2Clr = h1.style.color;
var s2Nmb = 0;

window.addEventListener("keydown", function (e) {
    var keycode = String.fromCharCode(e.keyCode);
    var isNmbr = Number(keycode);
    if (s2Clr == 'white' && (isNmbr || keycode == 0)) {
        s2.textContent = String.fromCharCode(e.keyCode);
        s2.style.color = 'rgb(192, 33, 33)';
        s2Clr = 'rgb(192, 33, 33)';
        window.s2Nmb = Number(s2.textContent);
    } else if (s1Clr == 'white' && (isNmbr || keycode == 0)) {
        s1.textContent = String.fromCharCode(e.keyCode);
        s1.style.color = 'rgb(192, 33, 33)';
        s1Clr = 'rgb(192, 33, 33)';
        window.s1Nmb = Number(s1.textContent);
    } else if (m2Clr == 'white' && (isNmbr || keycode == 0)) {
        m2.textContent = String.fromCharCode(e.keyCode);
        m2.style.color = 'rgb(192, 33, 33)';
        m2Clr = 'rgb(192, 33, 33)';
        window.m2Nmb = Number(m2.textContent);
    } else if (m1Clr == 'white' && (isNmbr || keycode == 0)) {
        m1.textContent = String.fromCharCode(e.keyCode);
        m1.style.color = 'rgb(192, 33, 33)';
        m1Clr = 'rgb(192, 33, 33)';
        window.m1Nmb = Number(m1.textContent);
    } else if (h2Clr == 'white' && (isNmbr || keycode == 0)) {
        h2.textContent = String.fromCharCode(e.keyCode);
        h2.style.color = 'rgb(192, 33, 33)';
        h2Clr = 'rgb(192, 33, 33)';
        window.h2Nmb = Number(h2.textContent);
    } else if (h1Clr == 'white' && (isNmbr || keycode == 0)) {
        h1.textContent = String.fromCharCode(e.keyCode);
        h1.style.color = 'rgb(192, 33, 33)';
        h1Clr = 'rgb(192, 33, 33)';
        window.h1Nmb = Number(h1.textContent);
    }
});
var myVar;
btn1.onclick = function () {
    window.myVar = setInterval(myTimer, 980);
}
btn2.onclick = function () {
    clearInterval(window.myVar);
}
btn3.onclick = function () {
    window.location.reload();
}
function myTimer() {
    //s2
    if (s2Nmb != 0) {
        if (s2Nmb == 0) {
            s2Nmb = 10;
        };
        s2Nmb--;
        s2.textContent = s2Nmb;
    } else {
        s2Nmb = 9;
        s2.textContent = s2Nmb;
        if (s2Nmb == 0) {
            s2Nmb = 10;
        };
    }
    //s1
    if (s2Nmb == 9) {
        if (s1Nmb == 0) {
            s1Nmb = 6;
        };
        s1Nmb--;
        s1.textContent = s1Nmb;

    }

    //section Minutes
    //m2
    if (s1Nmb == 5 && s2Nmb == 9) {
        if (m2Nmb == 0) {
            m2Nmb = 10;
        };
        m2Nmb--;
        m2.textContent = m2Nmb;

    }
    //m1
    if (m2Nmb == 9 && s1Nmb == 5 && s2Nmb == 9) {
        if (m1Nmb == 0) {
            m1Nmb = 6;
        };
        m1Nmb--;
        m1.textContent = m1Nmb;

    }
    //h2
    if (m1Nmb == 5 && m2Nmb == 9 && s1Nmb == 5 && s2Nmb == 9) {
        if (h2Nmb == 0) {
            h2Nmb = 10;
        };
        h2Nmb--;
        h2.textContent = h2Nmb;

    }
    //h1
    if (h2Nmb == 9 && m1Nmb == 5 && m2Nmb == 9 && s1Nmb == 5 && s2Nmb == 9) {
        if (h1Nmb == 0) {
            h1Nmb = 6;
        };
        h1Nmb--;
        h1.textContent = h1Nmb;

    }
    //alert
    if (h1Nmb == 0 && h2Nmb == 0 && m1Nmb == 0 && m2Nmb == 0 && s1Nmb == 0 && s2Nmb == 0) {
        clearInterval(window.myVar);
        var snd = new Audio("alarm_beep.wav"); 
        snd.volume = 0.3;
        snd.play();
    }

};
\$\endgroup\$
2
\$\begingroup\$

Timer accuracy

JS timers only guarantee that at least delay milliseconds elapsed before your function is called. It does not guarantee the exact time when. What happens under the hood when you call setInterval or setTimeout is that your callback gets queued for execution. JS only picks the callback up when it has cleared the execution stack. If something is holding up the JS engine, that callback maybe called later than expected.

Timer implementation

Implementation of timers also vary. If I remember correctly, the fastest interval is never less than 4ms for regular timers. So a 0ms delay is never really 0ms. Browser vendors may slow down or stop timers entirely timers when the window is not in focus or when it detects inactivity to save battery.

Guaranteeing timer accuracy

One trick to guarantee time accuracy is to never actually rely on the timer. Instead, you look at the real time. To do that, you create a timer that runs faster than a second. On each invocation, call Date.now() and compare the timestamp.

// Implementing a timer using just the timer
var intervalInput = document.getElementById('interval')
var intervalSeconds = 0
setInterval(() => {
  intervalInput.value = ++intervalSeconds
}, 1000)

// Implementing a by computing seconds from real time since the start.
var realInput = document.getElementById('real')
var start = Date.now() 
setInterval(() => {
  // Compute how many seconds since we started
  realInput.value = Math.floor((Date.now() - start) / 1000)
}, 200)
<label>Interval seconds:</label> <input id="interval">
<label>Real seconds:</label> <input id="real">

Time representation

A naive implementation of a timer involves storing the the units (seconds, minutes, hours, etc) in separate variables, and write a cascading logic (i.e. from 59s to 0s, increments mins) to update the values. This is not really scalable once you start adding more units. It's also hard to debug since you need to step through that logic.

A better way to do this is to store time in a single value, like milliseconds, and compute the units off of it. The units are just functions of the millisecond value. A countdown timer is simply a future timestamp as start value minus the current timestamp.

const hoursInput = document.getElementById('hours')
const minutesInput = document.getElementById('minutes')
const secondsInput = document.getElementById('seconds')
const startButton = document.getElementById('start')
const pauseButton = document.getElementById('pause')
const resetButton = document.getElementById('reset')

const pad = v => `00${v}`.slice(-2)

// Functions that compute units from a millisecond value
const hours = m => Math.floor((m / 3600000) % 24)
const minutes = m => Math.floor((m / 60000) % 60)
const seconds = m => Math.floor((m / 1000) % 60)

// Render function
const render = v => {
  hoursInput.value = pad(hours(v))
  minutesInput.value = pad(minutes(v))
  secondsInput.value = pad(seconds(v))
}

// This is the value you ask from the user
const duration = 3610000

let remaining = duration
let timer = null
let end = 0

startButton.addEventListener('click', e => {
  if (timer) return

  // Get end timestamp based on current time + remaining time
  end = Date.now() + remaining

  // Render the remaining time every 16ms (approx 60fps)
  timer = setInterval(() => {
    render(end - Date.now())
  }, 16)
})

pauseButton.addEventListener('click', e => {
  // Do nothing if timer not running
  if(!timer) return
  
  // Otherwise, clear timer
  clearInterval(timer)
  timer = null
  
  // Note the remaining time
  remaining = end - Date.now()
  render(remaining)
})

resetButton.addEventListener('click', e => {
  // Do nothing if timer not running
  if(!timer) return
  
  // Otherwise, clear timer
  clearInterval(timer)
  timer = null
  
  // Reset remaining to original duration
  remaining = duration
  render(remaining)
})

render(remaining)
<input id="hours">h <input id="minutes">m <input id="seconds">s

<button type="button" id="start">Start</button>
<button type="button" id="pause">Pause</button>
<button type="button" id="reset">Reset</button>

Lastly, one minor flaw in your countdown timer is that your start button doesn't check if the timer is already running. This causes it to spawn another JS timer, doubling the decrement.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.