深入解析 JavaScript 浏览器事件循环机制:从进程到宏微任务

作者:像素漫游

时间:2026-10-23

浏览:235

本文详细解析 JavaScript 在浏览器中的事件循环机制。从进程与线程的基础概念入手,阐述浏览器内核中 GUI 渲染、JS 引擎等线程的协作与互斥关系。重点讲解同步与异步任务的划分,以及宏任务(Macro-task)和微任务(Micro-task)在 Event Loop 中的执行优先级与顺序,通过示例代码剖析控制台输出结果,帮助开发者彻底理解 JS 单线程模型下的异步处理逻辑。

文章目录

首先梳理相关的基础概念。

进程与线程的基础概念

进程与线程的基本定义

进程是系统分配独立资源的单位,也是 CPU 资源分配的基本单元,它由一个或多个线程组成。

线程是进程内的执行流,作为 CPU 调度和分派的基本单位。同一进程下的多个线程共享该进程的资源。

浏览器内核的多线程架构

浏览器内核架构

浏览器采用多进程架构,每个标签页通常对应一个独立的进程(尽管多个空白标签页可能会合并到同一个进程中)。浏览器内核,即浏览器渲染进程,是多进程架构中的一种关键进程。

浏览器内核内部运行着多个线程,协同工作。

GUI 渲染线程

  1. 该线程负责页面的渲染工作,包括解析 HTML 和 CSS 以构建 DOM 树等。当页面发生重绘或回流时,会触发该线程执行。
  2. GUI 渲染线程与 JS 引擎线程互斥。当 JS 引擎线程正在执行代码时,GUI 渲染线程会被挂起,GUI 的更新操作会被放入任务队列中,等待 JS 引擎线程空闲后继续执行。

JS 引擎线程

  1. 该线程以单线程模式工作,主要负责解析和执行 JavaScript 脚本。
  2. 它与 GUI 渲染线程互斥。如果 JS 执行时间过长,会导致页面阻塞,影响用户体验。

事件触发线程

当事件满足触发条件时,该线程会将对应的回调函数添加到任务队列的队尾,等待 JS 引擎处理。

定时器触发线程

  1. 浏览器的定时计数器并非由 JS 引擎计数,因为 JS 阻塞会导致计时不准确。
  2. 浏览器开启专门的定时器触发线程进行计时。计时结束后,会将对应的事件添加到任务队列中,等待 JS 引擎处理。

HTTP 请求线程

  1. 发起 HTTP 请求时,会开启一条独立的请求线程。
  2. 当请求完成并获取结果后,会将请求的回调函数添加到任务队列中,等待 JS 引擎处理。

 进程与线程的基础概念

JavaScript 引擎的单线程特性

JavaScript 引擎的单线程特性

JavaScript 引擎是单线程的,这意味着每次只能执行一项任务,其他任务必须按顺序排队等待。只有当前任务执行完毕后,才会执行下一个任务。

HTML5 提出了 Web-Worker API,旨在解决页面阻塞问题,但这并未改变 JavaScript 单线程的本质。开发者应了解 Web-Worker 的使用场景。

事件循环机制概述

JavaScript 事件循环机制概述

JavaScript 的事件循环机制分为浏览器环境和 Node.js 环境,两者的实现技术不同。浏览器的 Event Loop 由 HTML 规范定义,而 Node.js 的 Event Loop 由 libuv 库实现。本文主要讨论浏览器环境下的机制。

JavaScript 拥有一个主线程(main thread)和一个调用栈(call-stack,也称执行栈)。所有任务都会被放入调用栈,等待主线程执行。

JS 调用栈的工作原理

JS 调用栈是一种后进先出(LIFO)的数据结构。当函数被调用时,它会被压入栈顶;执行完成后,该函数从栈顶弹出,直到栈被清空。

同步与异步任务的执行流程

同步任务与异步任务的区分

在 JavaScript 单线程模型中,任务分为同步任务和异步任务。同步任务按顺序在调用栈中排队,等待主线程执行。异步任务则在产生结果后,将注册的回调函数添加到任务队列(消息队列)中,等待主线程空闲(即调用栈清空)时,从队列读取并压入栈中执行。任务队列遵循先进先出(FIFO)原则。

Event Loop 运行机制

当调用栈中的同步任务全部执行完毕,栈被清空,表示主线程空闲。此时,Event Loop 机制会从任务队列中按顺序读取一个任务放入栈中执行。每次栈清空后,都会检查任务队列是否有任务,若有则读取执行,这种“读取-执行”的循环过程构成了事件循环。

进程与线程的基础概念

进程与线程的基础概念

定时器的行为机制

定时器会开启一条定时器触发线程进行计时。在等待指定时间后,定时器会将事件放入任务队列,等待主线程读取并执行。

定时器指定的延时毫秒数并不绝对准确。因为定时器仅在指定时间后将事件放入任务队列,必须等待同步任务和现有任务队列中的事件全部执行完毕后,才会读取定时器事件到主线程执行。如果中间存在耗时较长的任务,就无法保证在指定时间精确执行。

宏任务与微任务的执行顺序

宏任务与微任务的细分

除了广义的同步和异步任务,JavaScript 单线程中的任务可以细分为宏任务(macro-task)和微任务(micro-task)。

宏任务(macro-task)包括:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering。

微任务(micro-task)包括:process.nextTick、Promises、Object.observe、MutationObserver。


console.log(1);
setTimeout(function() {
  console.log(2);
})
var promise = new Promise(function(resolve, reject) {
  console.log(3);
  resolve();
})
promise.then(function() {
  console.log(4);
})
console.log(5);

在示例代码中,setTimeout 和 Promise 被称为任务源。来自不同任务源注册的回调函数会被放入不同的任务队列中。

有了宏任务和微任务的概念后,JavaScript 的执行顺序是怎样的?是宏任务先执行还是微任务先执行?

在第一次事件循环中,JavaScript 引擎会将整个 script 代码作为一个宏任务执行。执行完成后,会检测本次循环中是否存在微任务。如果存在,则依次从微任务的任务队列中读取并执行完所有微任务,然后再读取宏任务任务队列中的任务执行,接着再执行所有的微任务,如此循环。JS 的执行顺序遵循每次事件循环中的“宏任务-微任务”模式。

  1. 以上述示例代码为例,在第一次事件循环中,整段代码作为宏任务进入主线程执行。
  2. 遇到 setTimeout 时,会在等待指定时间后,将回调函数放入宏任务的任务队列中。
  3. 遇到 Promise 时,会将 then 函数放入微任务的任务队列中。
  4. 整个事件循环完成后,会检测微任务的任务队列中是否存在任务,如果存在则执行。
  5. 第一次循环的结果打印顺序为:1, 3, 5, 4。
  6. 接着,从宏任务的任务队列中按顺序取出一个宏任务到栈中让主线程执行。在这次循环中,宏任务是 setTimeout 注册的回调函数。执行完该回调函数后,发现本次循环中不存在微任务,准备进行下一次事件循环。
  7. 检测到宏任务队列中已没有要执行的任务,事件循环结束。
  8. 最终的控制台输出结果为:1, 3, 5, 4, 2。

以上是对 JavaScript 浏览器事件循环机制的详细解析,希望对大家有所帮助。如有任何疑问,欢迎留言,小编会及时回复。感谢大家对本站的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章