setTimeout(fn, 0) 为什么不是立即执行?
setTimeout(fn, 0) 的回调不会立即执行,因为 setTimeout 是异步 API。即使延迟时间设为 0,浏览器仍然会将回调函数放入宏任务队列中,等待当前调用栈中的同步代码执行完毕且微任务队列清空后,才会在下一轮事件循环 tick 中取出执行。延迟时间 0 只表示"尽快执行",而不是"立即执行"。在事件循环的视角下,同步代码始终是最先执行的,任何异步回调都必须等待同步代码完成。这就是为什么在 setTimeout(fn, 0) 之前定义的同步代码一定会先于 setTimeout 的回调执行。
Promise.then 和 setTimeout 哪个先执行?
Promise.then 的回调作为微任务,总会比 setTimeout 的回调作为宏任务先执行。当两者在同步代码中同时注册时,事件循环会在同步代码执行完毕后先清空微任务队列(执行 Promise.then 的回调),然后再从宏任务队列中取出一个宏任务(执行 setTimeout 的回调)。这个执行顺序是确定的,不受延迟时间或代码位置的影响。理解这一点对于预测和调试异步代码的执行顺序非常重要。如果在代码中同时使用了 Promise 和 setTimeout,回调的执行顺序一定遵循"微任务优先"的原则。
async/await 和 Promise.then 的执行顺序一样吗?
async/await 和 Promise.then 的执行顺序在语义上是等价的,但在细节上有一些差异。await 表达式之后的代码被包装为微任务,执行时机与 Promise.then 的回调相同。但多个 await 表达式的执行方式与多个 then 链式调用有所不同:每个 await 会暂停当前 async 函数的执行,将后续代码注册为微任务。如果在同一个 async 函数中有多个 await,它们不会在一轮微任务中全部执行完毕,而是每次只执行到下一个 await 处暂停。而 Promise.then 链中的每个回调在上一个完成后立即作为微任务注册。实际效果上,两者的最终执行顺序通常是相同的。
微任务队列为空时才执行宏任务吗?
是的,微任务队列的清空是宏任务执行的前置条件。事件循环的流程是:从宏任务队列取出一个宏任务执行 -> 清空微任务队列(包括在执行过程中新产生的微任务)-> 判断是否需要渲染 -> 渲染(如果需要)-> 从宏任务队列取出下一个宏任务执行。这个流程保证了微任务的执行优先级始终高于下一个宏任务。如果在清空微任务的过程中产生了新的微任务,它们也会在本轮被处理,不会等到下一轮。但微任务的执行不会阻塞渲染,如果在清空微任务后浏览器判断需要渲染,会先执行渲染操作再处理下一个宏任务。
微任务中死循环会导致页面卡死吗?
会的。如果在微任务中产生了无限递归或死循环,事件循环会在清空微任务队列时陷入无限循环,导致宏任务永远无法执行,渲染也永远不会触发。这会导致页面完全冻结,无法响应用户交互和刷新操作。因为微任务的"全清空"机制要求所有微任务(包括在执行过程中新产生的微任务)都被处理完毕后才能继续,如果微任务的产生速度永远超过处理速度,就会造成死循环。因此,在使用 queueMicrotask 或在 Promise 回调中执行代码时,一定要避免产生无限循环的微任务。
requestAnimationFrame 在事件循环中的哪个阶段执行?
requestAnimationFrame 的回调在浏览器判断需要渲染时执行,执行时机在微任务队列清空之后、实际渲染操作之前。在事件循环的一个 tick 中,处理顺序是:执行一个宏任务 -> 清空微任务队列 -> 执行 requestAnimationFrame 回调 -> 执行渲染(计算样式、布局、绘制等)。这意味着 requestAnimationFrame 的回调在微任务之后执行,但它不会像宏任务那样需要排队等待。requestAnimationFrame 的执行频率与浏览器的刷新频率同步(通常是 60fps),如果浏览器不需要重绘(比如页面没有变化),则不会执行 requestAnimationFrame 的回调。
Node.js 的事件循环和浏览器有什么不同?
Node.js 的事件循环由 libuv 库实现,采用阶段化(phased)的设计,比浏览器的事件循环更加复杂。Node.js 的事件循环分为多个阶段:timers 阶段处理 setTimeout/setInterval 回调、poll 阶段处理 I/O 回调、check 阶段处理 setImmediate 回调、close 阶段处理关闭事件回调。每个阶段之间还有微任务处理阶段。Node.js 的微任务处理也有自己的特点:process.nextTick 的优先级高于 Promise 微任务。而浏览器没有 setImmediate,渲染操作是浏览器特有的。理解这些差异对于在不同环境中编写正确的异步代码非常重要。
如何避免事件循环被阻塞?
避免事件循环被阻塞的核心原则是不要在同步代码中执行耗时操作。具体方法包括:(1)将耗时的计算拆分为多个小任务,使用 setTimeout 或 requestAnimationFrame 分帧执行,每帧之间留出让事件循环处理其他任务的时间;(2)将 CPU 密集型计算放到 Web Worker 中执行,Web Worker 有独立的事件循环不会阻塞主线程;(3)使用 requestIdleCallback 在浏览器空闲时执行低优先级任务;(4)避免在循环中执行 DOM 操作和复杂计算,尽量批量处理 DOM 更新。通过这些方法可以确保事件循环保持畅通,页面始终能响应用户交互和进行流畅渲染。
UD5工具箱