再再次去搞懂事件循环

核心概念

JavaScript是单线程语言,这意味着它只有一个主线程来处理所有任务。这种设计避免了多线程环境复杂的线程同步问题,尤其是在操作DOM 时多线程可能带来灾难性的竞态条件 但是单线程也有问题,如果遇到需要等待的操作,如网络请求,定时器等,整个线程都被阻塞,用户界面会完全卡住,这显然是不行的。 为了解决这个问题,事件循环机制应运而生。它让JavaScript能够非阻塞的处理异步任务,通过任务队列和循环调度高效执行。

关键原理

理解事件循环机制首先要理解三个概念

  1. 调用栈 调用栈是一个后进先出 的数据结构,负责跟踪当前正在执行的函数(同步代码 )。函数被调用时,会被压入栈顶;执行完毕后,被弹出
    • 同步任务:直接进入调用栈执行
    • 如果调用栈中任务过多,会导致栈溢出
  2. 任务队列 任务队列是一个先进先出 的数据结构,用于存储异步任务的回调函数。这些回调函数按照事件的触发顺心被放入任务队列中,等待事件循环处理。任务队列分两种:
    • 宏任务队列 :包括setTimeout,setInterval,I/O操作,UI渲染,事件回调
    • 微任务队列 :包括 Promise.thenPromise.catchPromise.finallyMutationObserverqueueMicrotask,以及 Node.js 中的 process.nextTick
  3. 事件循环 事件循环是链接调用栈任务队列 的桥梁,它持续运行检查是否有待处理的任务,基本职责是:
    • 检查调用栈是否为空
    • 如果调用栈为空,从任务队列中取出一个任务(回调函数)并推入执行栈执行
    • 重复上述过程

事件循环的工作流程

  1. 执行当前调用栈中的所有同步任务:所有同步代码都会立即执行,直到调用栈清空
  2. 清空微任务队列:一旦调用栈空闲,事件循环会优先处理微任务队列中的所有任务,包括在此期间新产生的微任务(这意味着微任务队列会被一次性彻底清空)
  3. 执行一个宏任务 :从宏任务队列中取出最早的一个任务执行(注意:每次循环通常只执行一个宏任务
  4. 重复步骤 2 和 3 :在执行完一个宏任务后,不会立即取下一个宏任务,而是再次回到步骤 2,清空可能新产生的微任务队列,然后再取下一个宏任务,如此循环 简单记忆:同步任务 → 所有微任务 → 一个宏任务 → 所有微任务 → 下一个宏任务 → ...

🖥️ UI 渲染的时机

在浏览器环境中,页面渲染(UI更新)通常发生在微任务队列处理完毕之后、下一个宏任务执行之前。这意味着,如果在微任务中进行了大量的计算或无限循环,将会阻塞UI渲染,导致页面卡顿。

常见问题

避免阻塞事件循环

  • 长任务 :同步任务或长时间运行的微任务会阻塞线程。解决办法是将大任务拆分小块 ,利用setTimeoutsetImmediate将其分解为多个宏任务
js 复制代码
function processLargeTask() {
    const chunkSize = 100;
    let index = 0;
    function processChunk() {
        // 处理数据分片...
        index += chunkSize;
        if (index < data.length) {
            // 使用宏任务分解避免阻塞
            setTimeout(processChunk, 0);
        }
    }
    processChunk();
}
  • 微任务风暴:在微任务中递归添加微任务会导致事件循环无法进入宏任务,造成死循环和界面卡死
js 复制代码
// 危险!会导致事件循环死锁
function infiniteMicrotask() {
    Promise.resolve().then(infiniteMicrotask);
}
infiniteMicrotask(); // 此后,任何宏任务都无法执行

定时器的不准确性

setTimeout并不能保证精确的延迟后执行,它只是表示'至少'延迟这么长时间。作为一个宏任务,如果遇到主线程或微任务队列被阻塞,实际执行时间会远大于设定延迟。 如果需要高精度场景,建议使用requestAnimationFrame[[从卡顿到流畅:前端渲染性能深度解析与实战指南]]

理解Promise构造器的执行顺序

Promise 构造器内部的代码是同步执行 的,只有 thencatchfinally 的回调是异步的(微任务)。

js 复制代码
console.log('1');
new Promise((resolve, reject) => {
    console.log('2'); // 这里是同步的!
    resolve();
}).then(() => {
    console.log('3'); // 这里是微任务
});
console.log('4');
// 输出: 1 → 2 → 4 → 3

💎 总结与核心要点

  1. 同步优先,异步靠后:同步任务总是最先在执行栈中执行。
  2. 微任务优先于宏任务:在当前同步任务和宏任务执行完后,必须清空所有微任务队列(包括新产生的),才会执行下一个宏任务。
  3. 事件循环是循环过程:每次循环处理一个宏任务和所有微任务。
  4. async/await 基于 Promise:其本质是微任务。
  5. 避免阻塞:警惕长时间运行的同步任务和微任务循环,它们会阻碍交互和渲染。

图表

相关推荐
代码搬运媛1 天前
Jest 测试框架详解与实现指南
前端
counterxing1 天前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq1 天前
windows下nginx的安装
linux·服务器·前端
之歆1 天前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜1 天前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai108081 天前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
candyTong1 天前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构
kyriewen1 天前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor
humcomm1 天前
元框架的工作原理详解
前端·前端框架
canonical_entropy1 天前
Attractor Before Harness: AI 大规模开发的方法论
前端·aigc·ai编程