I’m puzzled by the behavior of the following code snippet. I have a button that becomes disabled while running a long computation after it’s clicked.
function delay() {
console.log('start');
for (let x = 0; x < 4000000000; x++) {
Math.sqrt(20);
}
console.log('done');
}
function click() {
button.disabled = true;
setTimeout(() => {
delay();
button.disabled = false;
}, 0);
}
const button = document.getElementById('button');
button.addEventListener('click', click);
The button successfully disables and the long computation runs, but something strange happens: if I click the button multiple times while it is in its disabled state, it queues up a single click event and runs the click() function again after it’s done with the current computation.
My understanding was that disabled elements should not trigger or queue any events. Could anyone explain this behavior?
I’ve read up on JavaScript’s microtasks and macrotasks as well as how the UI updates in between them. Based on this, I was expecting the button to be fully unresponsive to clicks while it’s disabled.
However, what I observed is that despite the button being disabled, if I click it multiple times while it’s disabled, a single click event still gets queued up.
2
Answers
it’s related to how browsers handle rapid sequences of events and the disabled state of an element.
When you click on the button rapidly, even if it’s disabled, the browser might still register the click event due to the way it processes events in rapid succession. This is especially true if the button becomes disabled and then re-enabled in a short time frame, as is the case with your code.
Here’s a breakdown of what’s happening:
You click the button.
The click function is called.
The button is immediately disabled.
The setTimeout function schedules the delay function to run after the current call stack is cleared.
While the button is disabled, you click on it again. The browser might still register this click event due to the rapid sequence of events.
The delay function runs, and after it’s done, the button is re-enabled.
The queued click event is processed, even though it was registered while the button was disabled.
To avoid this behavior, you can use a flag to check if the computation is already running and prevent the function from being called again:
With this modification, even if the browser registers a click event while the button is disabled, the click function will immediately return without doing anything because the isRunning flag is set to true.
async/await
.requestAnimationFrame
to your timeout logic. For example if you try to display thestart
message withsetTimeout(..., 0)
the UI won’t be updated. That’s because the next rendering frame is in the future, but you block the rendering with the loop, so the frame waits you to return the control to the browser. UsingrequestAnimationFrame
makes you waiting the frame rendered and then starting the loop.