JavaScript 事件循环与宏微任务执行机制详解

Jasper
Jasper
发布时间:2026-10-15| 3人浏览过
原创

JavaScript 作为单线程脚本语言,其核心在于事件循环机制如何协调同步与异步任务。本文剖析浏览器渲染进程中的线程协作原理,对比宏任务与微任务的执行差异,通过实例解析异步回调的执行顺序。

为什么 JavaScript 是单线程的

JavaScript 主要用作浏览器脚本语言,核心用途是用户交互和操作 DOM。若存在多线程,多个线程同时修改同一 DOM 元素会导致不可预测的结果(如一个线程添加内容而另一个移除)。为避免此类冲突并简化模型,JavaScript 被设计为单线程运行。

浏览器内部进程与线程架构

现代浏览器采用多进程架构以提高稳定性和安全性。每个 Tab 页通常对应一个独立的渲染进程。

浏览器主要进程组成

  • 浏览器主进程:负责界面显示、用户交互、页面管理、网络资源管理及将渲染结果绘制到屏幕。
  • 第三方插件进程:隔离插件崩溃风险,防止影响浏览器主体。
  • GPU 进程:处理 3D 绘制等图形加速任务。
  • 渲染进程(Renderer):即浏览器内核,负责页面渲染、脚本执行和事件处理。这是 JavaScript 运行的环境,也是我们需要关注的重点。
  • 网络进程:主要负责页面资源的加载。

进程间数据严格隔离,一个进程的崩溃不会牵连其他进程。在渲染进程中,JavaScript 的执行依赖于以下几个关键线程的协作:

渲染进程内的关键线程

GUI 线程

负责解析 HTML/CSS,构建 DOM 树和 Render 对象树,进行布局和绘制。涉及重绘(Repaint)和重排(Reflow/Refist),这两者开销较高应尽量避免。GUI 线程与 JS 引擎线程互斥:当 JS 引擎执行时,GUI 线程挂起;JS 空闲时,GUI 线程才会更新界面队列中的待渲染内容。

JS 引擎线程

由 V8 等引擎实现,负责解析和执行 JavaScript 代码。无论何时,一个 Tab 页中只有一个 JS 线程在运行。由于它与 GUI 线程互斥,如果 JS 执行时间过长,会导致页面渲染阻塞,造成卡顿。

事件触发线程

不属于 JS 引擎,而是浏览器内核的一部分,用于管理任务队列(Task Queue)。当发生鼠标点击、Ajax 请求或定时器到期等异步事件时,该线程将对应的回调函数放入任务队列队尾,等待 JS 引擎线程空闲时处理。

定时触发器线程

负责 setInterval 和 setTimeout 的计时。由于 JS 引擎是单线程且可能阻塞,计时不由 JS 引擎完成。计时完毕后,回调会被添加到事件触发线程的任务队列中。注意,W3C 规范规定超时时间低于 4ms 按 4ms 计算。

异步 HTTP 请求线程

XMLHttpRequest 请求在新线程中发起。当检测到状态变更时,若设置了回调,则将回调加入事件队列,等待 JS 引擎执行。

事件循环(Event Loop)机制

JavaScript 将任务分为同步任务和异步任务。同步任务在主线程执行栈中依次执行;异步任务则由上述特定线程处理后,将回调放入任务队列。当执行栈清空后,JS 引擎会读取任务队列,将可执行的回调压入执行栈开始运行,这种反复查询和执行的过程即为事件循环。

let setTimeoutCallBack = function() {
  console.log('我是定时器回调');
};
let httpCallback = function() {
  console.log('我是http请求回调');
}

// 同步任务
console.log('我是同步任务1');

// 异步定时任务
setTimeout(setTimeoutCallBack,1000);

// 异步http请求任务
ajax.get('/info',httpCallback);

// 同步任务
console.log('我是同步任务2');

上述代码的执行逻辑如下:

  1. 执行同步代码:console.log('我是同步任务1')。
  2. 遇到 setTimeout,将其回调交给定时器线程。1秒后,定时器线程将回调加入事件触发线程的任务队列。
  3. 遇到 ajax 请求,交由异步 HTTP 线程。请求完成后,回调加入任务队列。
  4. 执行同步代码:console.log('我是同步任务2')。
  5. 此时主线程执行栈为空,JS 引擎检查任务队列。若队列中有回调,则将其推入执行栈立即执行;若为空,则继续轮询等待。

各线程的行为总结

  • 定时触发线程:只负责计时,定时结束后将回调移交事件触发线程。
  • 异步 HTTP 请求线程:只负责请求,结束后将回调移交事件触发线程。
  • 事件触发线程:仅将异步回调加入任务队列。
  • JS 引擎线程:执行执行栈中的代码,栈空后询问任务队列是否有待处理事件,循环往复。

宏任务与微任务

为了更精细地控制异步任务的执行时机,宏任务(Macrotask)和微任务(Microtask)的概念被引入。它们并非 JS 原生标准最初的一部分,而是通过消息队列机制实现的。

宏任务(Macrotask)

宏任务是由调度系统触发的任务集合。常见的宏任务包括:

  • 整体代码 script
  • setTimeout
  • setInterval
  • 用户交互事件(鼠标点击、滚动等)
  • I/O 操作
  • requestAnimationFrame:通常在宏观任务执行完后、下一个宏观任务开始前执行,用于保证渲染流畅。

微任务(Microtask)

微任务是在当前宏任务执行结束后、下一宏任务开始前立即执行的任务。其特点是优先级高于宏任务。常见的微任务包括:

  • Promise.then(), .catch(), .finally()
  • process.nextTick() (Node.js)
  • MutationObserver

当一个宏任务执行完毕后,JS 引擎会先清空当前的微任务队列,执行所有微任务,然后再考虑 UI 渲染或进入下一个宏任务。

执行流程总结

  1. 执行一个宏任务(初始为整体代码)。
  2. 在执行过程中产生的微任务,加入到微任务队列。
  3. 当前宏任务执行完毕。
  4. 清空微任务队列,依次执行所有微任务。
  5. 进行 UI 渲染(如需)。
  6. 进入下一个宏任务。

经典面试题分析

通过分析以下代码,验证宏微任务的理解:

function test() {
  console.log(1)
  setTimeout(function () { // timer1
    console.log(2)
  }, 1000)
}

test();

setTimeout(function () { // timer2
  console.log(3)
})

new Promise(function (resolve) {
  console.log(4)
  setTimeout(function () { // timer3
    console.log(5)
  }, 100)
  resolve()
}).then(function () {
  setTimeout(function () { // timer4
    console.log(6)
  }, 0)
  console.log(7)
})

console.log(8)

第一阶段:同步代码执行

  1. 调用 test(),打印 1。timer1(1秒延迟)被注册为宏任务。
  2. 外部 setTimeout(timer2, 0) 被注册为宏任务。
  3. 执行 new Promise 构造函数,同步代码 console.log(4) 打印 4。timer3(100ms延迟)被注册为宏任务。
  4. Promise 立即 resolve(),.then() 作为微任务加入微任务队列。
  5. 执行末尾 console.log(8),打印 8。

此时输出结果为:1, 4, 8。

第二阶段:第一次微任务清理

  1. 同步代码执行完毕,开始执行微任务队列。
  2. 执行 promise.then() 中的回调:
    • 注册 timer4(0ms延迟)为宏任务。
    • 打印 console.log(7),输出 7。

此时输出结果为:1, 4, 8, 7。

第三阶段:宏任务队列执行

此时宏任务队列中有 timer2, timer4, timer3, timer1。按照注册时间及最小延时阈值排序:

  • timer2: 注册于初始阶段,设定 0ms。虽然后续可能有微小延迟,但在当前 tick 中它是最早完成的候选之一。
  • timer4: 注册于 timer2 之后(因为 timer2 先于 Promise 定义),但设定也是 0ms。根据浏览器调度,通常按插入顺序处理相近时间的任务。
  • timer3: 设定 100ms。
  • timer1: 设定 1000ms。

实际上,timer2 和 timer4 都设定为 0ms,但 timer2 在前文更早被提交到定时器线程,因此优先触发:

  1. 执行 timer2:打印 3。无新微任务。
  2. 执行 timer4:打印 6。无新微任务。
  3. 执行 timer3(100ms 后触发):打印 5。无新微任务。
  4. 执行 timer1(1000ms 后触发):打印 2。

最终完整输出序列:

1
4
8
7
3
6
5
2

以上就是 JavaScript 事件循环与宏微任务执行顺序的详细内容,更多关于 JavaScript 异步编程的基础资料请关注本站其它相关文章!

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。