I'm making a simple countdown timer that displays the seconds and setseconds left. When an user clicks a button the countdown should stop and some text should be displayed instead of the timer. Everything works fine on pc but if I use my phone with a worse cpu I sometimes get this unexpected behaviour: when the user clicks the button the text shows for a fraction of a second and then the timer keeps going on. I thought it was caused by the clearTimeout() method being called when the function was running and right before a new timeout would be called but this post proved me wrong. Do you have any idea what's causing this behaviour and how should I go about fixing it?
const finished = () => { // finished() is also called when user clicks a button
// show some thext on the screen
clearTimeout(timeout);
}
const start = document.timeline.currentTime! // Think of it as Date.now(); also returns a ms value; just slightly faster.
const end = start 30000 // the countodwn takes 30 seconds
let timeout:any;
const frame = () => {
const elapsed = Math.floor(end - document.timeline.currentTime!)
if (elapsed <= 0) {
finished()
return
};
let secs: number | string = Math.floor(elapsed / 1000)
let setsecs: number | string = Math.floor((elapsed % 1000) * 0.1)
secs = secs < 10 ? "0" secs : secs
setsecs = setsecs < 10 ? "0" setsecs : setsecs
result.innerText = `Time left: ${secs}.${setsecs}`
timeout = setTimeout(() => requestAnimationFrame(frame), 15 - elapsed % 15) // raf is for optimisation and I'm substracting the timeout by the time that has already passed for drift minimisation
}
frame()
EDIT: I managed to recreate the slow behaviour on my phone with promises: Here's the code
CodePudding user response:
TL;DR
Problem
The time is too short between setTimeout
and clearTimeout
. A rescheduling is fired behind clearTimeout
.
Example
Use a boolean flag to check if that finished
function had been called.
Solution
const start = document.timeline.currentTime! // Think of it as Date.now(); also returns a ms value; just slightly faster.
const end = start 3000 // the countodwn takes 3 seconds
let timeout:any;
let finishedCalled = false
const finished = () => { // finished() is also called when user clicks a button
// show some thext on the screen
finishedCalled = true
clearTimeout(timeout)
console.log(`Finished!`)
}
setTimeout(finished, 2000)
const frame = async () => {
if(finishedCalled) return console.log(`Finished called found. Have to be end here.`)
const elapsed = Math.floor(end - document.timeline.currentTime!)
console.log(`Elapsed:`, elapsed)
if (elapsed <= 0) {
finished()
return
};
let secs: number | string = Math.floor(elapsed / 1000)
let setsecs: number | string = Math.floor((elapsed % 1000) * 0.1)
secs = secs < 10 ? "0" secs : secs
setsecs = setsecs < 10 ? "0" setsecs : setsecs
console.log(`Time left: ${secs}.${setsecs}`)
await new Promise((resolve) => setTimeout(resolve, 500));
timeout = setTimeout(() => {
console.log(`Timer fired!`)
requestAnimationFrame(frame)
}, 10) // raf is for optimisation and I'm substracting the timeout by the time that has already passed for drift minimisation
}
frame()
Dig Deeper
Let's use Node.js as an example and trace down the source.
clearTimeout
calledunenroll
unenroll
calledemitDestroy
emitDestroy
===emitDestroyScript
emitDestroyScript
calledasync_wrap.queueDestroyAsyncId
async_wrap.queueDestroyAsyncId
calledAsyncWrap::EmitDestroy
AsyncWrap::EmitDestroy
may or may not execute immediately.
Until this point, we can know it is an async call and may be rescheduled before execute. That's why the finished
function cannot stop the timer
.
Disclaimer: I am not an expert of C . Please feel free to improve the answer.