JavaScript 是一种单线程的编程语言,这意味着它一次只能处理一个任务。然而,现代前端应用需要处理大量异步操作(如网络请求、定时器、用户交互等),单线程如何实现非阻塞的异步编程?答案在于其独特的事件循环(Event Loop)机制。
JavaScript 的单线程设计源于其最初作为浏览器脚本语言的定位。为了避免多线程操作 DOM 带来的复杂性,JS 选择通过事件驱动模型处理并发。但单线程无法直接处理耗时操作(如等待 3 秒的定时器),否则会导致页面“冻结”。为此,浏览器环境提供了Web API(如 setTimeout
、fetch
),允许主线程将异步任务交给底层线程处理,自身继续执行后续代码。
事件循环的运作依赖三个核心组件:
调用栈(Call Stack)
记录函数的执行顺序,遵循后进先出(LIFO)原则。例如,函数 a()
调用 b()
,则 b()
先入栈执行,完成后 a()
继续执行。
任务队列(Task Queue)
存放异步任务完成后的回调函数。任务队列分为两种类型:
setTimeout
、setInterval
、DOM 事件回调等。Promise.then
、MutationObserver
、queueMicrotask
等。事件循环线程
持续检查调用栈是否为空,若为空则按优先级处理队列中的任务。
事件循环的运行遵循以下步骤:
执行同步代码
逐行执行主线程代码,遇到异步任务时将其交给 Web API 处理,继续执行后续代码。
处理微任务队列
当调用栈为空时,事件循环会一次性执行完所有微任务队列中的回调。微任务具有高优先级,因此会优先于宏任务执行。
渲染页面(如有需要)
浏览器可能在此阶段进行页面渲染(重排、重绘等)。
处理一个宏任务
从宏任务队列中取出一个任务执行,之后重复步骤 2-4,形成循环。
微任务优先于宏任务
setTimeout(() => console.log("宏任务"), 0);
Promise.resolve().then(() => console.log("微任务"));
// 输出顺序:微任务 → 宏任务
微任务嵌套导致阻塞
微任务执行期间产生的新的微任务会被追加到当前队列,导致宏任务被延迟:
function loopMicrotasks() {
Promise.resolve().then(loopMicrotasks);
}
loopMicrotasks();
setTimeout(() => console.log("此代码永远不会执行"), 1000);
渲染时机
页面渲染通常发生在微任务队列清空后、执行下一个宏任务前。若微任务长时间占用线程,页面会卡顿。
在 Node.js 中,事件循环分为多个阶段(如 timers
、poll
、check
),且微任务队列的优先级规则更复杂。例如:
process.nextTick
的优先级高于 Promise.then
。setImmediate
、I/O
操作等。JavaScript 通过事件循环机制,在单线程环境下实现了高效的异步编程。理解微任务与宏任务的执行顺序、避免长时间阻塞微任务队列,是优化应用性能的关键。掌握这一机制,能够帮助开发者更好地处理异步逻辑、避免潜在的性能陷阱。