关于js的事件循环,我相信凡是从事前端工作的开发者,都有一定程度的了解,但大多都是"背书",从"js是个单线程语言"开始,到"宏任务和微任务队列,微任务优先级更高"结束。 概念其实没什么大问题,但是随着浏览器逐渐演变成仅次于操作系统的复杂应用,我们的传统观念也需要一定的更新,今天,带大家从浏览器的视角出发,看看当下的事件循环是什么样子。
浏览器的进程模型
何为进程
程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程。 每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。
何为线程
有了进程后,就可以运行程序的代码了。 运行代码的「人」称之为「线程」。 一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程 。 如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。
浏览器有哪些进程线程
首先要明确一点:浏览器是一个多进程多线程的应用程序 。 浏览器内部工作极其复杂。为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。
可以在浏览器的任务管理器中查看当前的所有进程 其中,最主要的进程有:
-
浏览器进程
主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。
-
网络进程
负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。
-
渲染进程(本文重点讲解的进程)
渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。
默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。
将来该默认模式可能会有所改变,有兴趣的同学可参见chrome官方说明文档
渲染主线程是如何工作的
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画 60 次
- 执行全局 JS 代码
- 执行事件处理函数
- 执行计时器的回调函数
- ......
关于渲染进程为什么不使用多线程来处理这么多任务,我其实推荐大家可以去看看《你不知道的javascript》中卷第二章,里面详细讲述了js为什么被设计为单线程
要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务? 比如:
- 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
- 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
- 浏览器进程通知我"用户点击了按钮",与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
- ......
渲染主线程想出了一个绝妙的主意来处理这个问题:排队。
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务,这样一来,就可以让每个任务有条不紊的、持续的进行下去了。
整个过程,被称之为事件循环(消息循环)
何为异步
代码在执行过程中,会遇到一些无法立即处理的任务,比如:
- 计时完成后需要执行的任务 ------
setTimeout
、setInterval
- 网络通信完成后需要执行的任务 --
XHR
、Fetch
- 用户操作后需要执行的任务 --
addEventListener
如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于「阻塞」的状态,从而导致浏览器「卡死」

渲染主线程承担着极其重要的工作,无论如何都不能阻塞! 因此,浏览器选择异步来解决这个问题

使用异步的方式,渲染主线程永不阻塞
任务有优先级吗
我们都知道事件循环的过程中包含宏任务和微任务的说法,经常讲,事件循环往往从宏任务开始,但是在执行下一个宏任务前,我们需要先将本轮宏任务产生的微任务执行完毕。 那么任务有优先级吗?答案是没有 。 但是任务队列有。 根据 W3C 的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。 在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
- 浏览器必须 准备好一个微队列 ,微队列中的任务优先所有 其他任务执行 html.spec.whatwg.org/multipage/w...
宏任务队列已经无法满足当前的浏览器要求了
在目前 chrome 的实现中,至少包含了下面的队列:
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 微队列:用户存放需要最快执行的任务,优先级「最高」
- .....
以下是模拟浏览器任务调度的伪代码(由豆包生成),重点体现了三种队列的优先级关系和处理流程:
javascript
// 模拟浏览器的三种任务队列
const queues = {
microtasks: [], // 微任务队列(最高优先级)
inputQueue: [], // 交互队列(用户输入等)
timerQueue: [], // 延时队列(setTimeout/setInterval)
renderingQueue: [] // 渲染队列(额外补充,用于完整模拟)
};
// 任务调度器状态
let isProcessing = false;
// 模拟浏览器的任务处理主循环
function browserMainLoop() {
// 持续运行的事件循环
while (true) {
// 1. 先处理所有微任务(最高优先级)
processAllMicrotasks();
// 2. 检查是否需要渲染(通常在微任务后考虑)
if (shouldRender()) {
processRenderingTasks();
}
// 3. 处理高优先级任务:交互队列(用户输入优先于定时器)
if (queues.inputQueue.length > 0) {
processNextTask(queues.inputQueue);
continue;
}
// 4. 处理延时队列任务(优先级低于交互)
if (queues.timerQueue.length > 0) {
// 只处理已到期的定时器任务
const now = getCurrentTime();
const readyTimers = queues.timerQueue.filter(task => task.expires <= now);
if (readyTimers.length > 0) {
// 按到期时间排序,先处理最早到期的
readyTimers.sort((a, b) => a.expires - b.expires);
processNextTask(readyTimers);
continue;
}
}
// 5. 若没有任务,进入休眠等待新任务
waitForNewTasks();
}
}
// 处理所有微任务(执行到队列为空)
function processAllMicrotasks() {
while (queues.microtasks.length > 0) {
const microtask = queues.microtasks.shift();
executeTask(microtask);
}
}
// 处理单个任务队列中的下一个任务
function processNextTask(queue) {
if (queue.length === 0) return;
isProcessing = true;
const task = queue.shift();
executeTask(task);
isProcessing = false;
// 执行完一个任务后,再次检查微任务(微任务会在当前任务后立即执行)
processAllMicrotasks();
}
// 执行任务的具体逻辑
function executeTask(task) {
try {
task.callback(); // 执行任务的回调函数
} catch (error) {
reportError(error); // 处理任务执行中的错误
}
}
// 辅助函数:检查是否需要渲染
function shouldRender() {
// 简化逻辑:根据浏览器刷新频率(如60Hz约16ms一次)判断是否需要渲染
return getCurrentTime() - lastRenderTime > 16;
}
// 模拟添加任务的API(对应浏览器提供的API)
const browser = {
// 添加微任务(如Promise.then)
queueMicrotask(callback) {
queues.microtasks.push({ callback });
},
// 添加延时任务(如setTimeout)
setTimeout(callback, delay) {
const expires = getCurrentTime() + delay;
queues.timerQueue.push({ callback, expires });
},
// 添加交互任务(如click事件)
addInputTask(callback) {
queues.inputQueue.push({ callback });
},
// 添加渲染任务
requestAnimationFrame(callback) {
queues.renderingQueue.push({ callback });
}
};
核心优先级规则说明:
- 微任务队列(microtasks)优先级最高 - 无论其他队列是否有任务,当前执行栈空闲时会先清空所有微任务 - 对应API:
Promise.then
、queueMicrotask
、async/await
等 - 交互队列(inputQueue)次之 - 用户输入(点击、键盘等)任务优先级高于定时器,保证用户操作的响应速度 - 浏览器会优先处理用户交互,避免界面卡顿感
- 延时队列(timerQueue)优先级较低 -
setTimeout
/setInterval
任务仅在没有交互任务时才会被处理 - 定时器的实际执行时间可能比设定时间晚(受队列阻塞影响) - 渲染任务(renderingQueue)适时执行 - 通常在微任务处理后、其他任务执行前检查是否需要渲染 - 遵循显示器刷新率(如60fps),避免过度渲染消耗性能
这个伪代码简化了浏览器的实际实现,但核心逻辑符合现代浏览器(包括Chrome)的任务调度原则:优先保证用户交互响应速度,其次处理定时任务,而微任务则始终在当前任务周期内立即完成。
实际浏览器中,任务调度会更复杂,还会涉及任务优先级动态调整、线程池管理、节能策略等,但上述伪代码已能体现三种队列的核心优先级关系。
总结
本文主要从浏览器的渲染进程的视角出发,为大家讲解当前浏览器环境下 的事件循环是什么样的,如果正在阅读本文的你之前并不了解什么是事件循环,我这里推荐你阅读这篇文章,我相信读完以后,你一定能对事件循环有一定程度的了解。