一、什么是事件循环通俗解释JavaScript 是一门“单线程”语言意思是它只有一个大脑主线程同一时间只能干一件事。但是浏览器里有很多耗时的工作比如请求接口、定时器、用户点击。如果 JS 傻等这些耗时工作完成页面就会卡死。餐厅比喻你就是一个只有一双手的厨师单线程 JS。客人点了一份需要炖 2 小时的排骨你不可能站在灶台前等 2 小时。于是你定个闹钟转身去切菜、炒菜。这个“不断检查闹钟有没有响然后决定下一步干什么”的循环机制就是事件循环。二、事件循环的作用通俗解释它的核心作用就是协调同步任务和异步任务的执行。让耗时的操作在后台进行不阻塞主线程从而保证页面的流畅和用户的交互。餐厅比喻确保你既能把炖排骨的活儿安排出去又能同时给新来的客人端茶倒水让所有客人用户都觉得餐厅运转得很顺畅没有人被冷落。三、事件循环的流程餐厅运转机制JS 引擎的工作流程是极其死板且高效的它永远只做以下三件事的循环看锅里执行调用栈Call Stack里的同步代码。看备忘录如果锅里空了同步代码执行完立刻去清空微任务队列。看墙上的单子微任务清空后浏览器去渲染页面UI Render然后从宏任务队列里拿出一个任务放进锅里执行。回到第 1 步无限循环。四、微任务队列 (Microtask Queue)通俗解释它是VIP 快速通道。它的优先级极高。只要主线程一有空必须一次性、全部把微任务队列里的活儿干完才能去干别的。餐厅比喻你刚炒完一个菜突然发现灶台上有几滴油需要立刻擦掉或者你需要立刻把刚才的账单记在本子上。这些“顺手立刻要做的琐事”就是微任务。你必须马上全做完才能去看墙上的单子。实际开发举例Promise.then / catch / finally比如接口请求成功后你立刻要更新界面上的某个状态。MutationObserver监控 DOM 节点的变化。Vue 中的 nextTick当你修改了数据Vue 需要立刻更新 DOM它底层就是利用了微任务。五、宏任务队列 (Macrotask Queue)通俗解释它是普通排队区。优先级低于微任务。每次事件循环只允许从宏任务队列里拿出一个任务去执行。餐厅比喻墙上挂着很多单子比如10 分钟后烤个面包30 分钟后炖个鸡汤。你每次只能从墙上撕下一张单子开始做。做完之后你要先检查有没有“立刻要擦的灶台”微任务然后再撕下一张单子。实际开发举例setTimeout / setInterval比如轮询接口获取新消息或者倒计时。整体script标签你写的整个 JS 文件其实就是一个大的宏任务。用户的各种交互点击、滚动比如点击按钮触发的事件。六、示例console.log(1. 按钮被点击了);// 同步任务立刻执行setTimeout((){console.log(3. 定时器触发);// 宏任务延后执行},0);Promise.resolve().then((){console.log(2. 数据请求成功准备渲染);// 微任务同步执行完后立刻执行});七、进程与线程1. 什么是进程 (Process)通俗解释进程就像是一家独立的餐厅分店。它是操作系统分配资源如内存、CPU 时间的最小单位。每家分店都有自己独立的厨房、桌椅、仓库独立的内存空间。餐厅比喻如果这家分店突然停电了或者厨房起火了进程崩溃它只会影响自己绝对不会影响到街对面的另一家分店。前端实际开发场景在 Chrome 浏览器中每一个打开的标签页Tab就是一个独立的进程。这就是为什么你在开发时如果某个网页代码写崩了导致崩溃只会显示“哦糟糕”而你浏览器里打开的其他网页比如查资料的页面依然能正常浏览。2. 什么是线程 (Thread)通俗解释线程就像是餐厅里的具体员工如主厨、服务员、保洁。它是 CPU 调度和执行任务的最小单位。一个进程餐厅里可以包含多个线程员工大家共同配合完成餐厅的运转。餐厅比喻员工们共享这家餐厅的场地和食材共享进程的内存空间。如果主厨JS 引擎线程不小心把厨房点着了整个餐厅进程都会受到影响。前端实际开发场景我们在上一个问题中提到的渲染进程里就包含了多个线程JS 引擎线程、GUI 渲染线程、事件触发线程等。它们相互配合让你写的代码能顺利执行页面能顺利画出来。3. 进程与线程的核心对比对比维度进程 (Process)线程 (Thread)通俗比喻独立的餐厅分店餐厅里的具体员工核心定义操作系统分配资源的最小单位CPU 调度和执行的最小单位内存空间独立的内存空间互相隔离共享所属进程的内存空间稳定性极高。一个崩溃不影响其他进程较低。一个崩溃可能导致整个进程挂掉开销成本创建和切换的开销大较重创建和切换的开销小较轻前端场景浏览器的每个标签页、插件渲染进程里的 JS 引擎、GUI 渲染 等4. 实践指南JS 是单线程的这大大简化了我们的编程模型不用去处理复杂的多线程锁问题。浏览器是多线程的通过事件触发、定时器、网络请求等后台线程的配合加上我们上一节讲的事件循环Event Loop让单线程的 JS 也能“看似多线程”地高效处理异步任务。永远不要让你的 JS 主线程干“重体力活”。如果有大量计算比如处理几万条数据的排序尽量使用Web Worker。它相当于老板额外雇了一个帮厨在后台独立干活干完把结果交给主厨这样就不会阻塞页面的渲染了。八、浏览器主要线程1. GUI 渲染线程页面的“装修工”职责负责解析 HTML 和 CSS构建 DOM 树和 CSSOM 树计算页面布局最后把页面画绘制到屏幕上。餐厅比喻他就是餐厅的装修工兼传菜员。负责把菜单HTML/CSS变成真实的餐桌布置并把做好的菜端给客人。实际开发当你改变一个元素的背景色或者往列表里追加一条数据时就是他在工作。2. JavaScript 引擎线程餐厅的“主厨”职责负责解析和执行 JavaScript 代码处理各种事件和回调。餐厅比喻他是餐厅的主厨负责炒菜、处理复杂的订单。实际开发你写的for循环、接口请求后的数据处理、点击按钮触发的逻辑全都在他这里执行。⚠️ 核心考点GUI 线程与 JS 线程的“互斥关系”这两个线程是互斥的不能同时干活。为什么互斥想象一下如果主厨JS正在修改菜单上的菜名而传菜员GUI同时按着旧菜单给客人上菜客人不就吃错了吗为了保证数据的一致性浏览器规定当 JS 引擎在执行脚本时GUI 渲染线程会被暂停反之亦然。实际开发场景如果你写了一个极其复杂的for循环比如循环一百万次主厨JS被死死占用了传菜员GUI就只能干等着。这时候用户就会感觉页面“卡死”了点什么都没反应。这就是我们常说的主线程阻塞。3. 事件触发线程餐厅的“前台接待”职责当发生鼠标点击、键盘输入时它负责把这些事件包装成任务放到任务队列中等 JS 主厨空闲了再去处理。餐厅比喻前台接待员。客人按了呼叫铃点击按钮接待员不会自己去炒菜而是把单子记下来等主厨忙完手头的活再把单子递给他。4. 定时器触发线程餐厅的“闹钟管理员”职责专门负责setTimeout和setInterval。时间一到就把回调函数放进任务队列。餐厅比喻负责看闹钟的员工。老板说“10 分钟后烤个面包”他就定个闹钟。10 分钟后闹钟响了他把单子放进队列。注意如果这时候主厨还在忙别的菜这个面包依然得排队这就是为什么setTimeout设定的时间往往不够精准的原因。5. 异步 HTTP 请求线程餐厅的“外卖员”职责处理XMLHttpRequest或fetch等网络请求。在后台独立执行请求完成后把结果交给 JS 引擎。餐厅比喻专门去外面采购食材的员工。他出去买菜主厨不用在厨房里傻等。菜买回来了他把菜放在窗口主厨再拿去加工。九、浏览器事件循环避坑指南 坑位一setTimeout(fn, 0) 真的是“立刻”执行吗新手误区以为把延时设为 0代码就会像同步代码一样瞬间执行。避坑真相0ms只是“最小延迟”。浏览器遵循 HTML 规范通常会强制设置一个最小阈值通常是 4ms。而且它的回调必须乖乖排队等“当前宏任务 所有微任务”都执行完毕后才会从队列里被拿出来执行。实际开发建议如果你想让一段代码在 DOM 更新后立刻执行别用setTimeout应该使用微任务如Promise.then或者 Vue 中的nextTick。 坑位二在微任务中执行“重体力活”新手误区觉得微任务如Promise.then优先级高、执行快就把大量耗时的计算比如遍历几万条数据放在里面。避坑真相微任务是“加塞王者”它会一次性全部执行完。如果在微任务里死循环或进行大量计算会直接阻塞主线程导致浏览器无法进行 UI 渲染页面会严重卡顿甚至假死。实际开发建议如果有耗时操作尽量将其拆分成多个小块利用宏任务setTimeout分片处理给浏览器留出渲染和响应用户操作的时间。 坑位三把“回调函数”等同于“异步”新手误区看到函数作为参数传递就以为它是异步执行的。避坑真相回调函数只是代码的一种传递方式它既可以是同步的也可以是异步的。比如数组的forEach里传入的回调就是完完全全的同步代码必须等它执行完才会往下走。实际开发建议判断同步还是异步不要看它是不是回调要看它的底层机制是进入调用栈还是进入微任务/宏任务队列。 坑位四Promise 构造函数里的代码是异步的新手误区以为写在new Promise()里面的代码都会延后执行。避坑真相Promise的构造函数内部是同步代码会立刻执行只有它后面的.then()或.catch()里面的回调才是异步的微任务。实际开发建议在写Promise时要清楚区分哪些逻辑是立刻发生的哪些是等待状态的。 坑位五用 setTimeout 做动画新手误区用setInterval或setTimeout来改变元素位置实现动画效果。避坑真相定时器的执行频率和屏幕的刷新频率往往不同步会导致动画看起来卡顿、掉帧Jank。实际开发建议做动画请认准requestAnimationFramerAF。它会在浏览器每次重绘之前执行与屏幕刷新率完美同步保证动画丝滑。十、浏览器线程模型避坑指南 坑位一在 JS 主线程中执行“重体力活”新手误区为了图方便把大量的数据处理比如遍历几万条数据的排序、复杂的加密解密算法直接写在主线程里。避坑真相我们之前讲过GUI 渲染线程和 JS 引擎线程是互斥的。如果 JS 主线程被这些耗时任务死死霸占GUI 渲染线程就会被挂起。用户看到的现象就是页面彻底卡死点击按钮毫无反应甚至浏览器会弹出“页面已停止响应”的警告。实际开发建议永远不要让主线程干重活遇到计算密集型任务请果断使用Web Worker。把它扔给后台线程去算算完再通过postMessage把结果交还给主线程更新 UI。 坑位二频繁、零散地操作 DOM新手误区在for循环里每循环一次就去修改一次 DOM 元素的样式或内容。避坑真相每次修改 DOM都会触发 GUI 渲染线程重新计算布局Reflow/重排和绘制Repaint/重绘。频繁触发会导致主线程和 GUI 线程不断来回切换性能开销极大。实际开发建议尽量批量操作 DOM。比如先在内存中如使用DocumentFragment或字符串拼接把 HTML 结构组装好最后再一次性插入到页面中。或者利用现代框架Vue/React的虚拟 DOM 机制来自动优化。 坑位三滥用 will-change CSS 属性新手误区听说will-change能提升动画性能于是给页面上所有的元素都加上这个属性。避坑真相will-change会提前让浏览器的合成线程为元素创建独立的图层。图层虽然能加速渲染但非常消耗内存。滥用会导致内存飙升在移动端甚至可能直接导致页面崩溃。实际开发建议只在元素即将发生动画或变化前通过 JS 动态添加will-change属性动画结束后立刻移除它。好钢要用在刀刃上。 坑位四使用同步的 Ajax 请求新手误区在请求接口时不小心开启了同步模式XMLHttpRequest的async参数设为false。避坑真相同步请求会让 JS 主线程傻等网络响应。在网络状况差的情况下主线程会被长时间阻塞整个页面直接冻结直到数据返回。实际开发建议坚决抛弃同步请求全面拥抱异步请求如fetch、axios让异步 HTTP 请求线程在后台默默工作把回调函数扔进任务队列绝不阻塞主线程。 坑位五用 setTimeout 代替动画 API新手误区用setInterval或setTimeout来不断改变元素的left/top值试图实现平滑的动画。避坑真相定时器的触发频率和屏幕的刷新频率通常是 60Hz即约 16.6ms 一次很难完美对齐。这会导致动画掉帧、卡顿并且修改left/top会强制触发昂贵的重排Layout。实际开发建议做动画请认准requestAnimationFrame(rAF)。它由浏览器专门调度能保证回调在每次重绘前精准执行并且配合transform属性如translateX使用可以直接交由合成线程处理完全不阻塞主线程。