JavaScript 是单线程、非阻塞、异步 的语言,事件循环(Event Loop)就是 JS 引擎用来管理异步任务执行顺序的核心机制。
它决定了代码什么时候运行、异步任务如何排队、页面什么时候渲染,是理解 JS 异步编程的基础。
宏任务 和 微任务
事件循环把异步任务分为宏任务(Macrotask)和微任务(Microtask)。
执行优先级:
同步代码 > 微任务 > 宏任务。
宏任务(Macrotask)
每次事件循环只执行一个,执行完就去检查微任务队列。
比较常见的宏任务是:
script 整体代码、setTimeout/setInterval、AJAX/fetch 网络请求、I/O 操作、UI 渲染、postMessage、MessageChannel。
遵循先进先出(FIFO)的队列机制。
2. 微任务(Microtask)
微任务优先级高于宏任务 ,每次宏任务执行完,会清空当前所有微任务后,再进入下一个循环。
常见的微任务:
Promise.then/catch/finally、async/await(底层就是 Promise)、queueMicrotask()、MutationObserver。
其队列机制是一次性清空全部微任务,再执行后续逻辑。
标准执行流程
- 执行同步代码(主线程执行栈);
- 同步执行完,立即清空所有微任务(微任务队列全部执行);
- 微任务清空后,检查是否需要 UI 渲染(浏览器渲染线程工作);
- 渲染完成后,取出一个宏任务执行;
- 重复以上循环。
一句话口诀:同步 → 微任务 → 渲染 → 宏任务(循环)
浏览器渲染时机
1. 渲染触发时机
-
在微任务清空之后、下一个宏任务之前执行;
-
不是每次事件循环都一定渲染,浏览器有渲染帧率限制(约 16ms 一次,60fps),会合并多次 DOM 修改做一次渲染,避免频繁重绘重排。
2. 关键结论
- 微任务会阻塞渲染:微任务没执行完,页面绝对不会渲染;
- DOM 修改在微任务后统一更新:在微任务里修改 DOM,会在本次循环渲染阶段统一生效;
- 宏任务在渲染之后执行 :
setTimeout等宏任务,一定在页面渲染完成后才运行。
场景示例代码:
js
// 执行顺序:同步 → 微任务(Promise) → 渲染 → 宏任务(setTimeout)
console.log('同步1');
Promise.resolve().then(() => {
console.log('微任务');
document.body.style.background = 'red'; // DOM修改,渲染时生效
});
console.log('同步2');
setTimeout(() => {
console.log('宏任务'); // 最后执行
}, 0);
补充内容
- Node.js 与浏览器事件循环差异
- 浏览器:只有一个宏任务队列;
- Node:分为 timers、pending callbacks 等六个阶段,微任务在每个阶段切换时清空(Node 15+ 已对齐浏览器规范)。
- async/await 本质
- 就是 Promise 语法糖,await 后面的代码属于微任务,优先级高于定时器、网络请求。
- 性能影响
- 微任务过多会阻塞渲染,导致页面卡顿;避免在微任务中执行大量耗时逻辑。
- 渲染线程互斥
- JS 执行与 DOM 渲染共用同一个线程,微任务阻塞 → 渲染阻塞 → 页面卡顿。
1分钟面试简洁版
事件循环是 JS 单线程下管理异步任务的核心机制,核心是宏任务、微任务、渲染时机三者的执行顺序。
首先,JS 先执行同步代码,执行完后先清空所有微任务 ,比如 Promise、async/await;微任务全部执行完之后,浏览器才会执行UI 渲染。
渲染完成后,再取下一个宏任务执行,比如 setTimeout、网络请求,循环往复。
简单说顺序就是:同步代码 → 微任务 → 渲染 → 宏任务。
微任务优先级高于宏任务,而且会阻塞渲染,必须等微任务全部执行完,页面才会更新。宏任务一定在渲染之后执行。
日常开发中,需要优先用微任务处理实时逻辑,避免大量微任务阻塞页面渲染。