关于JS运行机制时长想起来就会刷一刷,然而长时间不看就会忘记这些概念的东西,所以好记性不如烂笔头,在查阅众多大佬文章后进行一个总结,方便后续回忆
JavaScript 以其单线程模型在浏览器环境中高效运行,但其异步处理能力却非常强大。理解其背后的运行机制,尤其是宏任务(Macro Task) 和微任务(Micro Task) ,对于编写高效、无阻塞的代码至关重要。要理解这些概念,我们首先需要了解其运行的底层基础:进程与线程。
一、进程与线程:计算机执行的基础单元
- 进程: 是计算机进行 CPU 资源分配的最小单位。你可以将其想象为一个独立的“工作车间”。每个进程拥有自己独立的内存空间和系统资源。当你打开一个浏览器标签页、一个文本编辑器或任何程序时,操作系统通常会为其创建一个或多个进程。
- 线程: 是 CPU 调度的最小单位,存在于进程内部。一个线程代表程序中的一个 执行流。一个进程可以包含多个线程(称为多线程),这些线程共享所属进程的内存和资源。线程是程序真正执行指令的“工人”。
二、JavaScript 的单线程宿命
JavaScript 被设计为 单线程 语言。这主要是由其核心用途决定的:与用户交互和操作 DOM(文档对象模型) 。
试想一下,如果 JavaScript 是多线程的:
- 一个线程试图在某个 DOM 节点上添加内容...
- 而另一个线程同时试图删除这个节点...
- 浏览器将陷入混乱,无法确定最终应该执行哪个操作,导致不可预测的结果和严重的同步问题。
为了避免这种复杂性,JavaScript 引擎选择只使用一个主线程来执行代码。这意味着在任意时刻,只能执行一个任务。那么,如何处理耗时操作(如网络请求、定时器)而不阻塞用户界面呢?答案就是 异步编程 和 事件循环(Event Loop) 。
三、浏览器多进程架构与渲染进程
现代浏览器本身是多进程的(例如,一个标签页可能对应一个渲染进程,还有浏览器主进程、GPU 进程、网络进程等)。负责执行 JavaScript、渲染页面、处理事件的,是其中的 渲染进程(也称为浏览器内核) 。
渲染进程内部又包含多个协作的线程:
-
GUI 渲染线程: 负责解析 HTML、CSS,构建 DOM 树、CSSOM 树,布局和绘制页面。
-
JavaScript 引擎线程(JS 主线程): 唯一执行 JavaScript 代码的线程。就是我们所说的“单线程”所指的那个线程。
-
事件触发线程: 管理事件(如点击、滚动)、维护 宏任务队列(Task Queue) ,并将队列中的任务通知 JS 主线程。
-
异步处理线程:
-
定时器触发线程:负责处理
setTimeout、setInterval。 -
HTTP 异步请求线程:负责处理
XMLHttpRequest、fetch等网络请求。 -
其他:如文件读取线程等。
-
这些线程不执行 JS 代码,主要负责处理异步操作的计时或 I/O,完成后通知事件触发线程。
-
四、代码执行流程:同步、异步与任务队列
一个 JavaScript 脚本的执行流程可以概括如下:
-
初始化与同步代码执行:
- JS 引擎开始执行脚本。
- 创建一个 全局执行上下文,其中维护着一个 微任务队列(Microtask Queue) 。
- 同步代码 被依次推入 JS 主线程的 执行栈(Call Stack) 中执行。
-
处理异步代码:
-
当 JS 主线程在执行同步代码时遇到 异步任务:
- 宏任务(如
setTimeout,setInterval, I/O, UI rendering, 事件回调) :JS 主线程将其交给对应的 异步处理线程(如定时器线程、HTTP请求线程)处理。主线程不会等待,而是继续执行后续同步代码。 - 微任务(如
Promise.then/catch/finally,MutationObserver,queueMicrotask) :JS 引擎本身会将这些异步操作产生的 回调函数 推入当前执行上下文(全局或函数)的 微任务队列 中。
- 宏任务(如
-
异步处理线程(如定时器线程)在异步操作(如计时结束、请求完成)完成后,会将对应的 回调函数 交给 事件触发线程。
-
-
事件触发线程与宏任务队列:
- 事件触发线程接收来自各种异步处理线程的回调通知(以及用户事件等)。
- 它将这些回调包装成 宏任务,并添加到 宏任务队列 中排队等待执行。
-
同步代码完成 & 微任务检查:
-
当 JS 主线程的 执行栈 中的同步代码全部执行完毕(执行栈清空)。
-
JS 引擎 不会立即 去执行宏任务队列中的任务。
-
引擎首先检查 当前执行上下文关联的微任务队列:
- 如果队列中有微任务,则 依次、全部 取出并推入执行栈执行,直到微任务队列清空。
- 这个阶段是 连续且不可中断的。
-
-
GUI 渲染 (可选):
- 微任务队列清空后,浏览器 可能 将控制权交给 GUI 渲染线程 进行页面的更新渲染(重排 Reflow、重绘 Repaint)。
- 注意:渲染时机由浏览器优化策略决定,不一定在每次微任务执行后都发生,但通常发生在一个宏任务及其关联的所有微任务执行完毕之后。
-
宏任务执行:
-
GUI 渲染(如果需要)完成后。
-
JS 引擎检查 宏任务队列。
-
如果队列中有宏任务,则取出 队列中的第一个宏任务,将其推入 JS 主线程的执行栈中执行。
-
这个宏任务执行过程中,又会重复步骤 1-6:
- 执行其内部的同步代码。
- 遇到异步任务,按规则处理(宏任务回调进宏任务队列,微任务回调进微任务队列)。
- 该宏任务本身的同步代码执行完毕。
- 再次清空当前执行上下文关联的微任务队列(所有微任务) 。
- 可能进行 GUI 渲染。
- 取下一个宏任务执行。
-
五、事件循环(Event Loop)与核心区别
上述步骤 4 到 6 的循环过程,就是著名的 事件循环(Event Loop) 。其核心是:
- 执行一个宏任务(从宏任务队列取)。
- 执行过程中产生的微任务,在 该宏任务执行结束、下一个宏任务开始之前 被 全部清空。
- 进行可能的 GUI 渲染。
- 取下一个宏任务执行。
宏任务与微任务的核心区别:
| 特性 | 宏任务 (Macro Task) | 微任务 (Micro Task) |
|---|---|---|
| 来源 | setTimeout, setInterval, I/O, UI事件, requestAnimationFrame (通常归类) | Promise.then/catch/finally, MutationObserver, queueMicrotask |
| 队列维护者 | 宿主环境(浏览器/Node.js) ,具体由 事件触发线程 管理 | JavaScript 引擎本身,在执行上下文(全局/函数)中维护 |
| 调度执行者 | 宿主环境 负责调度,通过事件循环机制通知 JS 主线程执行 | JS 引擎 在 当前执行上下文结束前 主动清空队列 |
| 执行时机 | 在 事件循环的下一轮(Tick) 中执行 | 在 当前宏任务执行完毕、下一个宏任务开始之前、可能的渲染之前 立即、连续、全部 执行 |
| 优先级 | 低 | 高 (在当前执行上下文中优先执行) |
六、总结
JavaScript 的单线程特性通过 事件循环机制 和 任务队列 实现了高效的异步处理。理解 宏任务 和 微任务 的区别及其执行顺序是掌握 JS 异步编程的关键:
- 同步代码 优先在主线程执行栈执行。
- 异步宏任务 由宿主环境的相关线程处理,回调进入宏任务队列,等待事件循环调度。
- 异步微任务 由 JS 引擎管理,回调进入微任务队列,在所属宏任务执行完毕后立即被清空执行(且优先于下一个宏任务和可能的渲染) 。
- 事件循环不断重复:
执行宏任务 -> 清空关联微任务 -> (可能渲染) -> 执行下一个宏任务。
这种机制确保了用户界面的响应性,同时允许开发者处理后台操作。牢记“微任务优先于宏任务执行”的原则,能帮助你更好地预测代码执行顺序,避免常见的异步陷阱。