再再次去搞懂事件循环

核心概念

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. 避免阻塞:警惕长时间运行的同步任务和微任务循环,它们会阻碍交互和渲染。

图表

相关推荐
艾小码7 小时前
还在拍脑袋估工时?3个技巧让你告别加班和延期!
前端·敏捷开发
UrbanJazzerati7 小时前
前端入门:vh、padding、margin、outline、pointer-events
前端·面试
wordbaby7 小时前
一行看懂高阶函数:用 handleConfirm 拿下 DatePicker 回调
前端·react.js
XiaoMu_0017 小时前
基于Node.js和Three.js的3D模型网页预览器
javascript·3d·node.js
卿·静7 小时前
Node.js对接即梦AI实现“千军万马”视频
前端·javascript·人工智能·后端·node.js
Mintopia7 小时前
🚀 Next.js 全栈 Web Vitals 监测与 Lighthouse 分析
前端·javascript·全栈
ITKEY_7 小时前
flutter日期选择国际化支持
开发语言·javascript·flutter
Mintopia7 小时前
🤖 AIGC + CMS:内容管理系统智能化的核心技术支撑
前端·javascript·aigc
HelloGitHub8 小时前
这款开源调研系统越来越“懂事”了
前端·开源·github