JavaScript 单线程却不阻塞,靠的就是事件循环机制。理解事件循环是搞懂 setTimeout「不准时」、Promise 执行顺序等问题的前提。
1. 运行模型
JS 引擎只有一个调用栈。异步任务不进入调用栈,而是由浏览器(或 Node)的其他线程处理,完成后把回调放入任务队列,等调用栈清空后事件循环再取出来执行。任务队列分两类:
宏任务(macrotask):setTimeout、setInterval、I/O、UI 渲染、script 整体代码。微任务(microtask):Promise.then/catch/finally、queueMicrotask、MutationObserver。
2. 执行顺序
每一轮事件循环:执行一个宏任务 → 清空全部微任务 → (浏览器环境)可能渲染 → 取下一个宏任务。微任务永远先于下一个宏任务执行:
console.log("1 同步");
setTimeout(() => console.log("2 宏任务"), 0);
Promise.resolve().then(() => console.log("3 微任务"));
console.log("4 同步");
// 输出:1 同步 → 4 同步 → 3 微任务 → 2 宏任务
即使 setTimeout 延迟 0,它的回调也要等同步代码和全部微任务执行完才会运行。
3. 嵌套场景
async function demo() {
console.log("a");
await Promise.resolve();
// await 之后的代码相当于放进微任务
console.log("b");
}
demo();
console.log("c");
// 输出:a → c → b
// demo 同步执行到 await 后让出,先走完外层同步代码
4. setTimeout 的最小延迟
HTML 标准规定嵌套超过 5 层的 setTimeout 最小延迟 4ms,浏览器对后台标签页还会把间隔放宽到 1 秒以上。所以 setTimeout(fn, 0) 的真实含义是「尽快,但不早于当前微任务清空后」。
5. 实用结论
需要「当前同步代码全部执行完再运行」的逻辑放微任务(Promise.then);定时、轮询放宏任务;动画和 UI 更新用 requestAnimationFrame,它在渲染前执行,比 setTimeout 更适合视觉任务。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END








暂无评论内容