事件循环(Event Loop)是 JavaScript 运行时的核心机制,也是面试和实际开发中都无法绕开的关键知识点。不理解 Event Loop,你就无法解释 setTimeout 的延迟为什么不准、为什么某些动画会卡顿、以及为什么 async/await 的行为有时会出乎意料。本文将系统性地解析 Event Loop 和页面渲染的关系。
JavaScript 的单线程本质
JavaScript 被设计为单线程语言,主要原因是作为浏览器脚本语言,它需要操作 DOM——如果有两个线程同时操作同一个 DOM 节点,结果将不可预测。但单线程带来了一个问题:如果一个任务执行时间过长,页面就会"卡住"。Event Loop 正是为了解决这个问题而设计的。
宏任务与微任务:事件循环的两层结构
事件循环中,任务分为两种优先级:宏任务(MacroTask)——包括 script 整体、setTimeout、setInterval、I/O、UI 渲染、postMessage 等,每次事件循环取一个执行。微任务(MicroTask)——包括 Promise.then/catch/finally、MutationObserver、queueMicrotask、process.nextTick 等,宏任务执行完后一次性清空所有微任务。
事件循环的完整流程
浏览器的事件循环可以概括为以下循环过程:(1) 从宏任务队列中取出一个宏任务执行;(2) 执行过程中产生的微任务放入微任务队列;(3) 当前宏任务执行完毕后,清空微任务队列(包括微任务中新产生的微任务);(4) 如果需要,执行 UI 渲染(requestAnimationFrame 回调在此阶段);(5) 回到步骤 1,进入下一轮事件循环。
console.log('1');\nsetTimeout(() => { console.log('2'); Promise.resolve().then(() => console.log('3')); }, 0);\nPromise.resolve().then(() => { console.log('4'); setTimeout(() => console.log('5'), 0); });\nconsole.log('6');\n// 输出:1 → 6 → 4 → 2 → 3 → 5async/await 的本质:Promise 的语法糖
async function foo() {\n console.log('A');\n await bar();\n console.log('B'); // await 后面的代码相当于 Promise.then\n}\n// 等价于:\nfunction foo() {\n console.log('A');\n return Promise.resolve(bar()).then(() => { console.log('B'); });\n}Event Loop 与页面渲染的关系
浏览器大约每 16.6ms(60fps)进行一次页面渲染。渲染发生在一轮事件循环的宏任务和微任务都执行完毕后。这意味着:微任务太多会阻塞渲染(如果微任务不断产生新的微任务,渲染将被无限推迟);requestAnimationFrame 在渲染前执行,适合做动画相关的计算;requestIdleCallback 在渲染后、下一轮循环前的空闲时间执行,适合做低优先级工作。
// 危险:死循环微任务会永久阻塞渲染\nfunction blockingMicroTask() {\n Promise.resolve().then(() => { blockingMicroTask(); });\n}\n// requestIdleCallback:在浏览器空闲时执行非关键任务\nrequestIdleCallback((deadline) => {\n while (deadline.timeRemaining() > 0 && tasks.length > 0) {\n processTask(tasks.shift());\n }\n});重排(Reflow)与重绘(Repaint)
重排(Reflow):元素的几何属性(位置、尺寸)发生变化 → 浏览器重新计算布局。触发操作包括修改 width/height、添加/删除 DOM、改变字体大小、读取 offsetHeight 等布局属性。重绘(Repaint):元素的视觉属性(颜色、背景)发生变化但不影响布局 → 只重新绘制。重排一定触发重绘,但重绘不一定触发重排。重排的成本远高于重绘。
// 错误:强制同步布局,每次读取 offsetHeight 都触发重排\nfor (let i = 0; i < 1000; i++) {\n element.style.width = i + 'px';\n const h = element.offsetHeight; // 强制浏览器立即重排\n}\n// 正确:批量读写分离\nconst width = element.offsetWidth; // 先读\nconst height = element.offsetHeight;\nelement.style.width = width + 10 + 'px'; // 后写\nelement.style.height = height + 10 + 'px';总结
理解事件循环和渲染机制后,你在写代码时会自然地做出更好的决策:长列表用虚拟滚动、动画用 requestAnimationFrame、非关键任务用 requestIdleCallback、合并 DOM 操作避免强制同步布局。这些看似微小的优化,积累起来就是用户体验的巨大差异。
评论 (0)