JavaScript 最初是为浏览器设计的,核心特点是单线程。这意味着它一次只能干一件事,就像快餐店里只有一个点餐台和一个服务员。如果顾客点了一个需要 10 分钟现做的汉堡,服务员傻等着,后面的顾客就得排队干瞪眼,体验极差。
在 JavaScript 里,耗时操作(比如网络请求、定时器)就像这个“现做汉堡”。如果主线程一直等着这些操作完成,页面就会卡住,用户点击、滚动等操作都无法响应。事件循环就是解决这个问题的“调度大师”,它让 JavaScript 在单线程的基础上实现异步处理,保证流畅的用户体验。
想象一家快餐店,事件循环的运行就像店里的工作流程,涉及以下四个核心部分:
setTimeout
、网络请求)。这些任务独立运行,不占用点餐台。这个“调度员”不断循环检查,形成事件循环,确保任务按序高效执行。
我们用一段代码来看看快餐店如何运转:
执行流程:
console.log('1. 开始点餐')
进入点餐台,打印 1. 开始点餐
,任务完成,出栈。setTimeout
进入点餐台,这是个耗时任务,服务员把它交给后厨(Web APIs)计时,回调函数暂存后厨,点餐台清空。console.log('2. 点了一杯可乐,瞬间拿到')
进入点餐台,打印 2. 点了一杯可乐,瞬间拿到
,任务完成,出栈。setTimeout
的回调放入普通取餐口(宏任务队列)。3. 汉堡做好了,取餐!
。输出顺序:
关键点:setTimeout
的 2000 毫秒只是表示“2 秒后将回调放入队列”,实际执行时间还取决于点餐台是否空闲。
快餐店的取餐口其实有两种:**普通取餐口(宏任务队列)**和 VIP 取餐口(微任务队列)。VIP 客户(微任务)总是优先于普通客户(宏任务)。
setTimeout
、setInterval
、DOM 事件回调、UI 渲染等,放在普通取餐口。Promise.then/catch/finally
、queueMicrotask
、MutationObserver
,放在 VIP 取餐口,优先级更高。事件循环的精细流程:
代码示例:
执行流程:
script start
。setTimeout
回调交给后厨,放入普通取餐口。Promise.resolve().then
回调放入 VIP 取餐口。script end
。.then
,打印 promise1
,它返回新 Promise,触发下一个 .then
,加入 VIP 取餐口。.then
,打印 promise2
。setTimeout
回调,打印 setTimeout
。输出顺序:
规则总结:一个宏任务 → 所有微任务 → 下一个宏任务。
事件循环在浏览器和 Node.js 环境中有些不同:
requestAnimationFrame
),微任务在渲染前执行。process.nextTick
,优先级高于其他微任务,且宏任务队列按类型分优先级(如 setImmediate
优先于 setTimeout
)。示例(Node.js):
原因:process.nextTick
优先于 Promise
,两者都先于 setTimeout
。
setTimeout(fn, 0)
** 不是立刻执行**:它只是尽快将回调放入宏任务队列,需等调用栈和微任务队列清空。Promise.then
)会延迟 UI 更新,影响用户体验。Promise
或 async/await
。setTimeout
分片执行。console.log
或调试工具(如 Chrome DevTools)跟踪任务执行顺序。事件循环是 JavaScript 异步编程的基石,通过调用栈、任务队列和事件循环协调单线程运行。它的核心规则是:
掌握事件循环,不仅能帮你理解代码执行顺序,还能优化性能、避免异步陷阱。无论是调试复杂前端逻辑,还是应对面试中的异步题目,它都是你的利器。