JavaScript setTimeout与事件循环机制深度解析

作者:流萤

时间:2026-10-25

浏览:588

本文深入解析JavaScript中setTimeout与事件循环(Event Loop)机制。通过对比浏览器与Node.js环境下的执行差异,详细阐述宏任务(macrotask)与微任务(microtask)的调度规则,并结合具体代码案例说明如何利用setTimeout实现特定的交互逻辑,帮助开发者规避异步编程中的常见陷阱。

文章目录

首先介绍大家熟知的setTimeout。

在前端或服务端开发中,setTimeout是常用的工具,主要用于实现代码的延迟执行。

    setTimeout(() => {
        console.log('延迟执行');
    },1000);

表面上看,它似乎没有太多复杂之处。

接下来探讨一些容易被忽视的setTimeout特性。

开发者有时会遇到一种情况:某些逻辑代码看似正确却无法生效,而加入setTimeout后程序却能正常运行。

这里提供一个实际项目中的具体应用场景。

需求场景为:点击按钮弹出模态框,点击模态框外部区域时模态框关闭,但点击模态框内部任何元素时,模态框保持显示不关闭。

<body>
        <button type="button" class="clickBtn">点击我</button>
        <div id="mask">
            <p class="title">提示</p>
            <p>
                <span class="area-content">这是一个弹框</span>
                <a href="javascript:;" rel="external nofollow" >跳转</a>
            </p>
        </div>
        <script type="text/javascript">
            document.querySelector('.clickBtn').addEventListener('click', () => {
                document.querySelector('#mask').style.display = 'block';
                setTimeout(() => {
                    window.addEventListener('click', hides, false);
                }, 0);
            });
            function hides(e) {
                document.querySelector('#mask').style.display = 'none';
                window.removeEventListener('click', hides, false);
            }

            document.querySelector('#mask').addEventListener('click', e => {
                e.stopPropagation();
            });
        </script>
    </body>

借助上述方法,利用setTimeout可以简洁地实现页面交互功能,避免了繁琐的e.target判断逻辑。

上述代码的关键在于setTimeout(()=>{},0)这段逻辑。要理解为何添加setTimeout能解决问题,而移除后会导致显示和隐藏异常,必须掌握一个核心概念:event-loop,即事件循环机制。

事件循环机制(event-loop)。

网络上已有许多关于event-loop的优质分析文章,文末会附上参考链接供进一步阅读。

本文试图从宏观角度再次梳理event-loop的核心逻辑。

需要注意的是,Node.js与浏览器中的event-loop实现机制不同。Node.js基于libuv库实现,而浏览器则由各大厂商各自实现。此处不讨论Node.js,仅聚焦于浏览器厂商对事件循环的处理方式。

需要明确几个关键点:既然称为事件循环,说明事件是循环执行的。一个事件循环包含一个或多个任务队列,每个任务队列中的任务严格按照先进先出(FIFO)的顺序执行,但不同任务队列之间的执行顺序是不确定的。

那么,哪些行为属于task或microtask呢?标准并未明确定义,但各类技术文章总结如下(不包含Node.js):

  • macrotasks,包括script(整体代码)、setTimeout、setInterval等。
  • microtasks,主要指Promise。

参考标准阐述的事件循环进程模型:

  • 1. 选择当前要执行的任务队列,选取最先进入队列的任务;若无任务可选,则跳转至microtask执行步骤。
  • 2. 将事件循环的当前运行任务设置为已选中的任务。
  • 3. 执行该任务。
  • 4. 将事件循环的当前运行任务重置为null。

从任务队列中移除已执行的任务:

  • 5. 进入microtask检查点(performing a microtask checkpoint)。
  • 6. 更新界面渲染。
  • 7. 返回第一步,继续循环。

当执行进入microtask检查点时,用户代理会执行以下步骤:

  • 设置进入microtask检查点的标志为true。
  • 当事件循环的微任务队列不为空时:选择一个最先进入microtask队列的microtask;设置事件循环的当前运行任务为已选中的microtask;运行该microtask;将当前运行任务重置为null;从microtask队列中移除已执行的microtask。
  • 对于相应事件循环的每个环境设置对象(environment settings object),通知其哪些promise被rejected。
  • 清理indexedDB的事务。
  • 设置进入microtask检查点的标志为false。

为了帮助理解,这里举一个具体的例子,因为上述理论可能较为晦涩。

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

需要注意的是,如果测试的浏览器不支持Promise或Promise/A+标准,或者使用了其他Promise polyfill,运行结果可能会有所差异。

预期的运行结果如下:

script start script end promise1 promise2 setTimeout

具体的步骤解释在附带的链接中有详细解读,此处不再赘述。

这里需要强调整个事件循环的工作流程:

在第一个事件循环中,只有一个任务,即JavaScript代码加载 -> 将setTimeout和promise分别放入macrotasks和microtask队列中 -> JavaScript执行完毕后,进入下一个事件循环 -> 执行microtask任务队列中的所有任务 -> 执行macrotasks中的一个任务,当前宏任务执行完毕后 -> 如果此时微任务队列不为空,则重新执行所有微任务;如果微任务为空,则执行下一个macroTask -> 完成,继续下一个事件循环。

现在回过头来看最初提到的弹框示例,就能明白为何setTimeout延迟0秒执行也能实现需求。根本原因在于setTimeout是一个task,必须在下一个时间周期执行。

setTimeout(() => {
   console.log(1); 
},0)

setTimeout(() => {
    console.log(2);
    Promise.resolve().then(() => console.log('promise1'));
    Promise.resolve().then(() => console.log('promise2'));
}, 0)
setTimeout(() => {
    console.log(3);
}, 0)

另一个测试的执行结果如下:

1 2 promise1 promise2 3

Node.js中的事件循环执行顺序。

总体而言,在Node 9版本之前,Node.js的实现与浏览器不同。就相同API部分而言,宏任务的执行顺序存在差异。例如,对于上述示例,在Node 8.x环境中执行,顺序如下:

1 2 3 promise1 promise2

在Node高版本中,其特性已与浏览器对齐。因此,在实际上线部署时,若涉及线上编译,务必注意Node.js的版本,以避免此类潜在问题。

关于事件循环中的Promise。

setTimeout相对简单直观,易于理解和使用。但当涉及Promise时,难度会增加。如果仅处理简单场景,Promise的使用无非是new Promise((resolve, reject)).then()的形式;然而,在复杂场景下,若不了解底层原理,很容易陷入陷阱。

至此,关于JavaScript的setTimeout与事件循环机制(event-loop)的介绍到此结束。更多关于JS setTimeout的内容,欢迎搜索本站以往文章或继续浏览相关文章,希望大家今后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章