深入解析 JavaScript 运行机制:单线程、事件循环与任务队列

62 阅读7分钟

关于JS运行机制时长想起来就会刷一刷,然而长时间不看就会忘记这些概念的东西,所以好记性不如烂笔头,在查阅众多大佬文章后进行一个总结,方便后续回忆

JavaScript 以其单线程模型在浏览器环境中高效运行,但其异步处理能力却非常强大。理解其背后的运行机制,尤其是宏任务(Macro Task)微任务(Micro Task) ,对于编写高效、无阻塞的代码至关重要。要理解这些概念,我们首先需要了解其运行的底层基础:进程与线程

一、进程与线程:计算机执行的基础单元

  • 进程:  是计算机进行 CPU 资源分配的最小单位。你可以将其想象为一个独立的“工作车间”。每个进程拥有自己独立的内存空间和系统资源。当你打开一个浏览器标签页、一个文本编辑器或任何程序时,操作系统通常会为其创建一个或多个进程。
  • 线程:  是 CPU 调度的最小单位,存在于进程内部。一个线程代表程序中的一个 执行流。一个进程可以包含多个线程(称为多线程),这些线程共享所属进程的内存和资源。线程是程序真正执行指令的“工人”。

二、JavaScript 的单线程宿命

JavaScript 被设计为 单线程 语言。这主要是由其核心用途决定的:与用户交互和操作 DOM(文档对象模型)

试想一下,如果 JavaScript 是多线程的:

  • 一个线程试图在某个 DOM 节点上添加内容...
  • 而另一个线程同时试图删除这个节点...
  • 浏览器将陷入混乱,无法确定最终应该执行哪个操作,导致不可预测的结果和严重的同步问题。

为了避免这种复杂性,JavaScript 引擎选择只使用一个主线程来执行代码。这意味着在任意时刻,只能执行一个任务。那么,如何处理耗时操作(如网络请求、定时器)而不阻塞用户界面呢?答案就是 异步编程 和 事件循环(Event Loop)

三、浏览器多进程架构与渲染进程

现代浏览器本身是多进程的(例如,一个标签页可能对应一个渲染进程,还有浏览器主进程、GPU 进程、网络进程等)。负责执行 JavaScript、渲染页面、处理事件的,是其中的 渲染进程(也称为浏览器内核)

渲染进程内部又包含多个协作的线程:

  1. GUI 渲染线程:  负责解析 HTML、CSS,构建 DOM 树、CSSOM 树,布局和绘制页面。

  2. JavaScript 引擎线程(JS 主线程):  唯一执行 JavaScript 代码的线程。就是我们所说的“单线程”所指的那个线程。

  3. 事件触发线程:  管理事件(如点击、滚动)、维护 宏任务队列(Task Queue) ,并将队列中的任务通知 JS 主线程。

  4. 异步处理线程:

    • 定时器触发线程:负责处理 setTimeoutsetInterval

    • HTTP 异步请求线程:负责处理 XMLHttpRequestfetch 等网络请求。

    • 其他:如文件读取线程等。

    • 这些线程不执行 JS 代码,主要负责处理异步操作的计时或 I/O,完成后通知事件触发线程。

四、代码执行流程:同步、异步与任务队列

一个 JavaScript 脚本的执行流程可以概括如下:

  1. 初始化与同步代码执行:

    • JS 引擎开始执行脚本。
    • 创建一个 全局执行上下文,其中维护着一个 微任务队列(Microtask Queue)
    • 同步代码 被依次推入 JS 主线程的 执行栈(Call Stack)  中执行。
  2. 处理异步代码:

    • 当 JS 主线程在执行同步代码时遇到 异步任务

      • 宏任务(如 setTimeoutsetInterval, I/O, UI rendering, 事件回调) :JS 主线程将其交给对应的 异步处理线程(如定时器线程、HTTP请求线程)处理。主线程不会等待,而是继续执行后续同步代码。
      • 微任务(如 Promise.then/catch/finallyMutationObserverqueueMicrotask :JS 引擎本身会将这些异步操作产生的 回调函数 推入当前执行上下文(全局或函数)的 微任务队列 中。
    • 异步处理线程(如定时器线程)在异步操作(如计时结束、请求完成)完成后,会将对应的 回调函数 交给 事件触发线程

  3. 事件触发线程与宏任务队列:

    • 事件触发线程接收来自各种异步处理线程的回调通知(以及用户事件等)。
    • 它将这些回调包装成 宏任务,并添加到 宏任务队列 中排队等待执行。
  4. 同步代码完成 & 微任务检查:

    • 当 JS 主线程的 执行栈 中的同步代码全部执行完毕(执行栈清空)。

    • JS 引擎 不会立即 去执行宏任务队列中的任务。

    • 引擎首先检查 当前执行上下文关联的微任务队列

      • 如果队列中有微任务,则 依次、全部 取出并推入执行栈执行,直到微任务队列清空。
      • 这个阶段是 连续且不可中断的
  5. GUI 渲染 (可选):

    • 微任务队列清空后,浏览器 可能 将控制权交给 GUI 渲染线程 进行页面的更新渲染(重排 Reflow、重绘 Repaint)。
    • 注意:渲染时机由浏览器优化策略决定,不一定在每次微任务执行后都发生,但通常发生在一个宏任务及其关联的所有微任务执行完毕之后。
  6. 宏任务执行:

    • GUI 渲染(如果需要)完成后。

    • JS 引擎检查 宏任务队列

    • 如果队列中有宏任务,则取出 队列中的第一个宏任务,将其推入 JS 主线程的执行栈中执行。

    • 这个宏任务执行过程中,又会重复步骤 1-6:

      • 执行其内部的同步代码。
      • 遇到异步任务,按规则处理(宏任务回调进宏任务队列,微任务回调进微任务队列)。
      • 该宏任务本身的同步代码执行完毕。
      • 再次清空当前执行上下文关联的微任务队列(所有微任务)
      • 可能进行 GUI 渲染。
      • 取下一个宏任务执行。

五、事件循环(Event Loop)与核心区别

上述步骤 4 到 6 的循环过程,就是著名的 事件循环(Event Loop) 。其核心是:

  1. 执行一个宏任务(从宏任务队列取)。
  2. 执行过程中产生的微任务,在 该宏任务执行结束、下一个宏任务开始之前 被 全部清空
  3. 进行可能的 GUI 渲染。
  4. 取下一个宏任务执行。

宏任务与微任务的核心区别:

特性宏任务 (Macro Task)微任务 (Micro Task)
来源setTimeoutsetInterval, I/O, UI事件, requestAnimationFrame (通常归类)Promise.then/catch/finallyMutationObserverqueueMicrotask
队列维护者宿主环境(浏览器/Node.js) ,具体由 事件触发线程 管理JavaScript 引擎本身,在执行上下文(全局/函数)中维护
调度执行者宿主环境 负责调度,通过事件循环机制通知 JS 主线程执行JS 引擎 在 当前执行上下文结束前 主动清空队列
执行时机在 事件循环的下一轮(Tick)  中执行在 当前宏任务执行完毕、下一个宏任务开始之前、可能的渲染之前 立即、连续、全部 执行
优先级高 (在当前执行上下文中优先执行)

六、总结

JavaScript 的单线程特性通过 事件循环机制 和 任务队列 实现了高效的异步处理。理解 宏任务 和 微任务 的区别及其执行顺序是掌握 JS 异步编程的关键:

  • 同步代码 优先在主线程执行栈执行。
  • 异步宏任务 由宿主环境的相关线程处理,回调进入宏任务队列,等待事件循环调度。
  • 异步微任务 由 JS 引擎管理,回调进入微任务队列,在所属宏任务执行完毕后立即被清空执行(且优先于下一个宏任务和可能的渲染)
  • 事件循环不断重复:执行宏任务 -> 清空关联微任务 -> (可能渲染) -> 执行下一个宏任务

这种机制确保了用户界面的响应性,同时允许开发者处理后台操作。牢记“微任务优先于宏任务执行”的原则,能帮助你更好地预测代码执行顺序,避免常见的异步陷阱。