提问人:justasking 提问时间:9/7/2023 更新时间:9/7/2023 访问量:80
了解 JavaScript 中禁用按钮上的排队点击事件
Understanding Queued Click Events on Disabled Button in JavaScript
问:
我对以下代码片段的行为感到困惑。我有一个按钮,在单击它后运行长时间的计算时被禁用。
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);
该按钮成功禁用并运行了长时间的计算,但发生了一些奇怪的事情:如果我在按钮处于禁用状态时多次单击该按钮,它会将一个单击事件排队,并在完成当前计算后再次运行 click() 函数。
我的理解是,禁用的元素不应触发或排队任何事件。谁能解释这种行为?
我已经阅读了 JavaScript 的微任务和宏任务,以及 UI 在它们之间如何更新。基于此,我预计该按钮在禁用时对点击完全无响应。
但是,我观察到的是,尽管按钮被禁用,但如果我在禁用时多次单击它,则单击事件仍然会排队。
答:
0赞
Florian
9/7/2023
#1
它与浏览器如何处理快速事件序列和元素的禁用状态有关。
当您快速单击该按钮时,即使它被禁用,浏览器仍可能注册单击事件,因为它处理事件的方式是快速连续的。如果按钮被禁用,然后在短时间内重新启用,则尤其如此,就像您的代码一样。
以下是正在发生的事情的细分:
你点击按钮。 调用 click 函数。 该按钮立即被禁用。 setTimeout 函数将延迟函数调度为在清除当前调用堆栈后运行。
当该按钮被禁用时,您再次单击它。由于事件的快速序列,浏览器可能仍会注册此单击事件。 延迟功能运行,完成后,按钮将重新启用。 将处理排队的单击事件,即使该事件是在禁用按钮时注册的。 若要避免此行为,可以使用标志来检查计算是否已在运行,并防止再次调用该函数:
let isRunning = false;
function delay() {
console.log('start');
for (let x = 0; x < 4000000000; x++) {
Math.sqrt(20);
}
console.log('done');
}
function click() {
if (isRunning) return;
isRunning = true;
button.disabled = true;
setTimeout(() => {
delay();
button.disabled = false;
isRunning = false;
}, 0);
}
const button = document.getElementById('button');
button.addEventListener('click', click);
通过此修改,即使浏览器在禁用按钮时注册了单击事件,单击函数也会立即返回,而无需执行任何操作,因为 isRunning 标志设置为 true。
评论
1赞
justasking
9/7/2023
即使进行了修改,行为也保持不变。即使我只在明显的延迟后单击一次禁用按钮,也没有任何变化。
0赞
Alexander Nenashev
9/7/2023
#2
- 会发生什么情况:当您使用循环阻止线程时,浏览器仍会收集事件。循环后,立即启用按钮,然后将控制权返回给浏览器。浏览器现在能够处理收集到的事件并启用您的按钮,因此它会触发事件,从而再次运行循环。解决方案:在单独的任务中也启用该按钮,以便浏览器在处理事件时仍将禁用该按钮。
- 您可以使用 .
async/await
- 若要使 UI 更新与代码同步,请添加到超时逻辑中。例如,如果尝试显示带有 UI 的消息,则不会更新。这是因为下一个呈现帧是将来的,但您使用循环阻止了呈现,因此该帧会等待您将控件返回给浏览器。使用会使您等待渲染的帧,然后开始循环。
requestAnimationFrame
start
setTimeout(..., 0)
requestAnimationFrame
const flushUI = () => new Promise((resolve => setTimeout(() => requestAnimationFrame(resolve))));
async function delay() {
const id = (Math.random()*100|0).toString().padStart(2, '0');
console.log(id, ': start');
await flushUI();
const start = performance.now();
for (let x = 0; x < 4000000000; x++) {
Math.sqrt(20);
}
console.log(id, ': done in', performance.now() - start + 'ms');
}
async function click() {
button.disabled = true;
await flushUI(); // could be omitted since delay() flushes too but doesn't hurt because delay() could be changed in the future and lose the flushing inside it
await delay();
await flushUI();
button.disabled = false;
}
const button = document.getElementById('button');
button.addEventListener('click', click);
button{
padding: 20px 30px;
border-radius:4px;
user-select:none;
cursor:pointer;
}
<button id="button">Click</button>
评论
delay
disabled
delay
delay
{once: true}
delay