前情提要
JavaScript作为浏览器端的脚本语言,自1995年诞生起就采用单线程模型。这种设计的核心考量在于避免多线程操作DOM引发的竞态条件------若两个线程同时修改同一DOM节点的样式或结构,可能导致页面渲染异常甚至崩溃。但单线程也带来了根本性矛盾:当遇到网络请求、定时器等耗时操作时,主线程会被阻塞,导致页面失去响应。
为解决这一矛盾,浏览器采用了多线程协作+事件驱动的架构。其中:
- 主线程:执行JS代码、处理DOM更新
- 辅助线程:包括网络线程、定时器线程、事件监听线程等,负责监听异步事件
- 任务队列:存储待执行的回调函数
这种架构通过事件循环(Event Loop)实现单线程下的异步非阻塞执行,成为现代Web应用高性能的核心保障。
为啥需要事件循环
浏览器环境的多线程特性
虽然JS执行是单线程的,但浏览器本身是多进程和多线程环境,以Chrome浏览器为例,我们可以从打开的浏览器工具中查看:
最主要的进程有以下3个:
- 浏览器进程(主进程)
是整个浏览器的核心,负责管理用户界面(如地址栏、书签栏、标签页),控制浏览器生命周期(例如创建和销毁标签页),并协调其他进程之间的通信。它还负责处理磁盘操作,例如下载文件、存储书签等,同时确保浏览器整体的顺畅运行。
- 渲染进程
专注于页面内容的解析和绘制。渲染进程启动后会开启一个渲染主线程,用于解析 HTML 和 CSS、执行 JavaScript 并管理 DOM 操作,最终将页面内容呈现到屏幕上。默认情况下,浏览器会为每一个标签页分配一个独立的渲染进程,以确保页面的隔离性和稳定性。
- 网络进程
负责处理所有与网络相关的任务,包括发起 HTTP/HTTPS 请求、WebSocket 通信,以及管理资源的下载和缓存。它独立于其他进程运行,确保即使网络任务出现问题,也不会影响页面的稳定性。
另外还有 GPU 进程 负责与 GPU 硬件交互,处理复杂的图形任务,插件进程 专门运行浏览器的插件等等。
除了多进程外,浏览器环境还有多线程特性,主要的线程有:
- GUI渲染线程:解析HTML/CSS、重绘界面(与JS线程互斥)
- 异步请求线程:处理XMLHttpRequest/Fetch
- 定时器线程:管理setTimeout/setInterval
- 事件触发线程:监听click/keydown等交互事件
在这些进程中,跟我们前端息息相关的就是渲染进程 ,因为它直接负责页面的解析、执行和渲染,是我们前端开发内容最终呈现的核心环节。而其中最重要的线程是渲染主线程,它也是浏览器中最繁忙的线程,承担了页面渲染和交互的大量核心任务。
当这些线程监听到事件时,会将回调函数推入对应队列,由事件循环调度执行
解决单线程阻塞问题
假设没有事件循环,以下代码将导致页面冻结3秒:
javascript
console.log("Start");
for(let i=0; i<3e9; i++){} // 模拟长任务
console.log("End");
通过事件循环机制,异步任务(如setTimeout)会被转移到其他线程处理,主线程继续执行后续代码,待异步任务完成后回调才进入队列。
任务队列(也称消息队列)
根据 W3C 规范和主流浏览器实现,现代浏览器采用 多队列模型管理异步任务。以下是浏览器任务队列的核心分类及其特性:
分类&特性
1. 微任务队列(Microtask Queue)
- 优先级:最高(必须优先清空)
- 典型任务 :
Promise.then()
/catch()
/finally()
回调MutationObserver
的 DOM 变更监听queueMicrotask()
手动添加的任务
- 执行时机:当前宏任务结束后立即执行,确保高优先级异步操作即时响应。
2. 交互队列(Interaction Queue)
- 优先级:高(用户交互优先)
- 典型任务 :
click
、scroll
、resize
等 DOM 事件回调- 媒体查询变化事件(如
mediaQuery.onchange
)
- 设计意义:优先响应用户操作以提升交互流畅度。
3.渲染前回调队列(Before-Render Queue)
- 优先级:次高(与下一帧渲染同步)
- 典型任务 :
requestAnimationFrame
回调- 部分浏览器实现的
IntersectionObserver
回调。
- 执行时机:下一帧渲染前执行,用于优化动画和布局计算。
4. 延时队列(Delay Queue)
- 优先级:中(受主线程阻塞影响)
- 典型任务 :
setTimeout
、setInterval
回调
- 特性:定时器到期后回调入队,执行时间不保证精确(±4ms 误差)。
5. 网络请求队列(Network Queue)
- 优先级:中(依赖资源加载速度)
- 典型任务 :
fetch
、XMLHttpRequest
响应回调- WebSocket 消息处理
- 流程:网络线程完成资源加载后,回调加入队列。
6. 空闲回调队列(Idle Queue)
- 优先级:低(仅空闲时执行)
- 典型任务 :
requestIdleCallback
回调- 后台日志上报等非关键任务
- 执行时机:主线程空闲且其他队列任务清空后执行。
执行顺序规则
- 微队列优先:每次事件循环必须清空微队列后,才处理其他队列任务。
- 队列间优先级排序:
微队列 → 交互队列 → 渲染前队列 → 延时队列 → 网络队列 → 空闲队列
- 同类型队列 FIFO :如多个
setTimeout
按注册顺序执行。
与传统宏/微任务模型的区别
- 旧模型局限性 :仅区分宏任务(如
script
、setTimeout
)和微任务(如Promise
),无法应对复杂场景。 - 新模型优势:任务类型细分 + 动态优先级调整(如用户滚动时提升交互队列优先级)。
说明:不同浏览器实现细节可能略有差异(如队列名称或优先级权重),但核心机制符合 W3C 规范要求。
JS执行过程
渲染主线程
前面说过,渲染主线程是浏览器中最繁忙的线程。它需要处理以下工作:
- 解析 HTML 和 CSS、计算样式、执行布局和绘制操作
- 管理页面的图层,将页面呈现到屏幕上
- 还需要执行 JavaScript,包括全局代码的执行、事件处理函数的回调、计时器的回调函数等
这些任务共同决定了页面的渲染性能和用户交互的流畅性,因此优化主线程的工作负载是前端性能优化的关键所在。
这些任务可能同时发生,但主线程一次只能执行一个任务。因此,任务的优先级和调度顺序成为了浏览器需要解决的核心问题。
浏览器通过任务队列 和事件循环的机制,将各种任务进行分类排队,按照一定的优先级和顺序逐一执行。这种排队机制既保证了主线程任务的有序性,又能够动态调整任务的优先级,从而在多种场景下提供最佳的用户体验。
无法立即处理的任务------异步
在浏览器运行中,有些操作是无法立即完成的,例如网络请求、定时器到期后的回调、文件读写等。 由于渲染主线程承担着极其重要的工作,无论如何都不能阻塞,如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于「阻塞」的状态,从而导致浏览器「卡死」。
为了避免这种情况,浏览器通过引入异步机制 ,将这些任务的等待过程交由消息队列管理,让渲染主线程继续执行其他任务。
异步任务的核心目标是:不阻塞主线程,同时确保任务的正确性和有序性。
例如,当代码执行到 setTimeout 时,浏览器不会立即执行其中的回调函数。相反,浏览器会将该回调注册到定时器模块中,开始计时。当计时器到达指定时间后,回调函数被放入消息队列中,等待主线程空闲时再取出并执行。这种设计允许主线程继续处理其他任务,同时确保回调函数在正确的时机被执行。
同样的逻辑适用于网络请求。当发起一个 fetch 请求时,主线程不会等待服务器的响应,而是继续执行后续代码。当网络模块收到服务器的响应数据后,会将处理响应的回调函数添加到消息队列,等待主线程处理。
异步任务的执行依赖于浏览器的事件循环机制
执行过程
任务有优先级吗?
在浏览器的事件循环和任务调度机制中,任务本身并没有明确的优先级,而是由消息队列 (task queue) 管理任务的执行顺序。 这一机制意味着虽然浏览器能够处理多种类型的任务(如用户输入、定时器回调、网络请求等),但任务的执行顺序并不是由任务本身的优先级来决定的,而是由消息队列的处理方式和队列中任务的类别、顺序来管理的。
这段话怎么理解呢?
首先,任务没有优先级。因为任务本身并不会被赋予明确的优先级标签。举个例子,如果你在浏览器中运行一个 JavaScript 脚本,它可能会触发一系列的事件(如 DOM 更新、网络请求的响应处理、定时器回调等)。这些任务的类型和来源不同,但它们都会按顺序被加入到消息队列中,等待主线程的处理。每个任务的执行时间和顺序都受限于它在队列中的位置,而不是任务本身的优先级。
虽然任务本身没有优先级,但是浏览器会通过将任务放入不同的消息队列的方式来管理任务的执行顺序。 根据 W3C 的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。
- 在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
在目前 chrome 的实现中,至少包含了下面的队列:
- 微队列:用户存放需要最快执行的任务,优先级「最高」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
目前来说我们只需要知道渲染主线程执行完全局 JavaScript 代码后会优先执行微队列中的任务。
微队列的任务总是优先于其他队列中的任务执行,每次事件循环结束时都会检查并清空微队列,它的优先级最高。
总结
JaveScript是一种单线的语言,渲染主线程同一时间只能处理一件事情,但是浏览器环境又是多线程的,可能同时存在各种异步任务(网络请求、鼠标点击、定时器...),为了不阻塞渲染主线的工作,引入了消息队用于管理这些异步任务,任务必须加到不同的消息队列中。主线程循环不断的从消息队列中取出任务执行,其中微队列的优先级最高,每次必须先将微队列中的任务执行完毕,才能从其他队列中取出其他任务。
本文部分描述参考文章:juejin.cn/post/743845...