状态锁似乎失效了,请问是为什么?

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <input type="checkbox" id="bigger_view" name="bigger_view" value="bigger_view" /><label for="bigger_view">切换</label>
</body>
<script>
    var bigger_view = document.querySelector("#bigger_view")
    var is_runing = false
    
    bigger_view.onchange = ()=>{
        if(is_runing){
            return
        }
        
        is_runing = true
        bigger_view.disabled = true
        
        run_task(()=>{
            is_runing = false
            bigger_view.disabled = false
        })
    }
    
    function run_task(callback){
        console.log("is_runing")
        for(var i=0; i<999999999*4; i++){
            
        }
        
        callback("finish")
    }
</script>
</html>

以上代码,点击 checkbox 后(此时 run_task 还未完成)快速再次点击,run_task 会执行两次,请问大家这是为什么呢?

阅读 1.2k
avatarAI BotBETA

这个问题是由于JavaScript在浏览器中运行时的单线程模型以及事件循环的工作方式导致的。在你的代码中,run_task 函数通过一个非常大的循环来模拟一个耗时的任务,尽管这个循环实际上并不执行任何计算密集型操作(即它只是空转),但它仍然会阻塞执行线程,使得UI更新(比如设置checkbox的disabled属性)和事件处理被延迟。

这里的关键点是,JavaScript运行在浏览器中时,它的执行环境是单线程的。这意味着在任何给定时间点,只有一个JavaScript代码块可以在执行栈上运行。浏览器还会维护一个任务队列(也称为事件队列),其中包含等待执行的事件处理程序和其他需要由JavaScript环境处理的任务。

当你第一次点击checkbox时,onchange事件处理程序开始执行。它首先检查is_runing标志(此时为false),然后将其设置为true,禁用checkbox,并调用run_task函数。由于run_task内部有一个长时间运行的循环,这个循环会阻塞线程,导致回调函数(callback)只有在循环完成后才会被调用。

然而,由于JavaScript的执行被阻塞,浏览器的UI更新(即checkbox的禁用状态)和事件队列的处理都被推迟了。如果在run_task的循环完成之前(即在callback执行之前),用户再次点击了checkbox,此时is_runing仍然为true(因为回调函数还未执行),但由于浏览器的事件循环机制,第二次点击的事件处理程序仍然会被加入到事件队列中等待执行。

run_task的循环完成后,它会调用回调函数,将is_runing设置为false并重新启用checkbox。但此时,第二个点击事件的处理程序已经在事件队列中等待,并且当浏览器回到执行JavaScript代码时,它会继续执行这个事件处理程序。由于此时is_runing已经再次被设置为false,第二次点击的处理程序将再次启动run_task,导致你观察到的行为。

要解决这个问题,你可以考虑使用更高级的异步处理方法,如Promiseasync/awaitsetTimeout等,来避免长时间阻塞UI线程。然而,对于简单的模拟或演示,你也可以通过限制点击操作的频率(例如使用防抖或节流技术)来避免这种重复执行的问题。

1 个回答
✓ 已被采纳

你可以添加一个点击事件:

bigger_view.onclick = () => {
  console.log(bigger_view.disabled);
};

可以看到,每次点击的时候 disabled 都是 false,这是由于「事件循环」,就像 AI 说的那样。

简单来说,浏览器运行 JS 时,会保存一个任务队列,主线程空闲时会不断取出队列中的任务并运行。更详细、更准确的描述请参考网上的资料。

将长 for 循环改为更加准确的 while 循环并控制它阻塞 5 秒:

  const start = Date.now();
  while (Date.now() - start < 1 * 5000);

点击 checkbox,等待 2 秒再次点击,将产生如下结果:

false
is_running
(5 秒后)
finish
false
is_running
(5 秒后)
finish

当你点击 checkbox 并触发 onchange 事件时,onchange 函数不会立即运行,而是将这个函数作为一个任务添加到任务队列中。由于第一个 onchange 事件还没有运行完成,所以第二个 onchange 函数要等第一个运行完才能执行。而第一个函数运行完成时,已经将 disabled 设置为 false 了。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题