核心概念
JavaScript是单线程语言,这意味着它只有一个主线程来处理所有任务。这种设计避免了多线程环境复杂的线程同步问题,尤其是在操作DOM 时多线程可能带来灾难性的竞态条件 但是单线程也有问题,如果遇到需要等待的操作,如网络请求,定时器等,整个线程都被阻塞,用户界面会完全卡住,这显然是不行的。 为了解决这个问题,事件循环机制应运而生。它让JavaScript能够非阻塞的处理异步任务,通过任务队列和循环调度高效执行。
关键原理
理解事件循环机制首先要理解三个概念
- 调用栈 调用栈是一个后进先出 的数据结构,负责跟踪当前正在执行的函数(同步代码 )。函数被调用时,会被压入栈顶;执行完毕后,被弹出
- 同步任务:直接进入调用栈执行
- 如果调用栈中任务过多,会导致栈溢出
- 任务队列 任务队列是一个先进先出 的数据结构,用于存储异步任务的回调函数。这些回调函数按照事件的触发顺心被放入任务队列中,等待事件循环处理。任务队列分两种:
- 宏任务队列 :包括
setTimeout
,setInterval
,I/O操作
,UI渲染
,事件回调
等 - 微任务队列 :包括
Promise.then
、Promise.catch
、Promise.finally
、MutationObserver
、queueMicrotask
,以及 Node.js 中的process.nextTick
- 宏任务队列 :包括
- 事件循环 事件循环是链接调用栈 和任务队列 的桥梁,它持续运行检查是否有待处理的任务,基本职责是:
- 检查调用栈是否为空
- 如果调用栈为空,从任务队列中取出一个任务(回调函数)并推入执行栈执行
- 重复上述过程
事件循环的工作流程
- 执行当前调用栈中的所有同步任务:所有同步代码都会立即执行,直到调用栈清空
- 清空微任务队列:一旦调用栈空闲,事件循环会优先处理微任务队列中的所有任务,包括在此期间新产生的微任务(这意味着微任务队列会被一次性彻底清空)
- 执行一个宏任务 :从宏任务队列中取出最早的一个任务执行(注意:每次循环通常只执行一个宏任务)
- 重复步骤 2 和 3 :在执行完一个宏任务后,不会立即取下一个宏任务,而是再次回到步骤 2,清空可能新产生的微任务队列,然后再取下一个宏任务,如此循环 简单记忆:同步任务 → 所有微任务 → 一个宏任务 → 所有微任务 → 下一个宏任务 → ...
🖥️ UI 渲染的时机
在浏览器环境中,页面渲染(UI更新)通常发生在微任务队列处理完毕之后、下一个宏任务执行之前。这意味着,如果在微任务中进行了大量的计算或无限循环,将会阻塞UI渲染,导致页面卡顿。
常见问题
避免阻塞事件循环
- 长任务 :同步任务或长时间运行的微任务会阻塞线程。解决办法是将大任务拆分小块 ,利用
setTimeout
或setImmediate
将其分解为多个宏任务
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
构造器内部的代码是同步执行 的,只有 then
、catch
、finally
的回调是异步的(微任务)。
js
console.log('1');
new Promise((resolve, reject) => {
console.log('2'); // 这里是同步的!
resolve();
}).then(() => {
console.log('3'); // 这里是微任务
});
console.log('4');
// 输出: 1 → 2 → 4 → 3
💎 总结与核心要点
- 同步优先,异步靠后:同步任务总是最先在执行栈中执行。
- 微任务优先于宏任务:在当前同步任务和宏任务执行完后,必须清空所有微任务队列(包括新产生的),才会执行下一个宏任务。
- 事件循环是循环过程:每次循环处理一个宏任务和所有微任务。
- async/await 基于 Promise:其本质是微任务。
- 避免阻塞:警惕长时间运行的同步任务和微任务循环,它们会阻碍交互和渲染。
图表
