前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • HTTP

    • 基础篇

      • HTTP的前世今生
      • HTTP是什么
      • HTTP世界全览
      • HTTP分层
      • 键入网址到回车发生什么
      • HTTP报文是什么样子的
      • 理解请求方法
      • URI
      • 响应状态码
      • HTTP有哪些特点
      • HTTP优缺点
      • HTTP的实体数据
      • HTTP传输大文件
      • HTTP的连接管理
      • HTTP的重定向
      • HTTP的Cookie机制
      • HTTP的缓存控制
      • HTTP的代理服务
      • HTTP的缓存代理
    • 高级篇

    • 扩展篇

  • 浏览器相关

  • 计算机基础

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

消息队列和事件循环:页面是怎么活起来的|浏览器篇

# 消息队列和事件循环:页面是怎么活起来的

30 秒速记

  • 浏览器渲染进程的主线程同时承担 DOM 处理、样式计算、布局、JavaScript 执行和输入事件响应。
  • 这些工作共享同一条主线程,需要统一的任务调度体系维持执行秩序。
  • 消息队列负责承接等待处理的任务,事件循环负责持续驱动主线程取出并执行任务。
  • 理解这套机制的核心价值,是解释页面为何能持续响应事件并推进不同类型的工作。
  • 主线程承载的任务种类多且繁忙;调度只能安排执行次序,并不能消除单线程上的工作负担。

页面依靠消息队列保存待处理任务,再由事件循环持续驱动渲染进程的主线程取出并执行任务。 因为 DOM 处理、样式计算、布局、JavaScript 执行和输入事件响应都共享主线程,所以需要统一调度来维持执行秩序。比如用户触发输入事件后,相关任务会等待主线程处理,页面也因此能够持续响应并推进工作。需要注意,调度只能安排任务的执行次序,不能消除单线程本身的工作负担。

前面我们讲到了每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是我们今天要讲的消息队列和事件循环系统。

在写这篇文章之前,我翻阅了大量的资料,却发现没有一篇文章能把消息循环系统给讲清楚的,所以我决定用一篇文章来专门介绍页面的事件循环系统。事件循环非常底层且非常重要,学会它能让你理解页面到底是如何运行的, 所以在本篇文章中,我们会将页面的事件循环给梳理清楚、讲透彻。

为了能让你更加深刻地理解事件循环机制,我们就从最简单的场景来分析,然后带你一步步了解浏览器页面主线程是如何运作的。

需要说明的是,文章中的代码我会采用 C++ 来示范。如果你不熟悉 C++,也没有关系,这里并没有涉及到任何复杂的知识点,只要你了解 JavaScript 或 Python,你就会看懂

面试官追问

追问 1商品详情页正在执行一段 JavaScript,同时收到点击、样式失效和布局更新请求;同事说这些工作会在主线程上并行执行,你怎么纠正?
参考回答

这些工作不会在同一个主线程上并行展开,而要经过消息队列与事件循环统筹,由主线程逐项处理。主线程同一时刻只能执行当前工作;材料没有展开真实浏览器的队列划分和优先级,因此不能进一步断言所有任务都严格按到达时间运行。

追问 2代码评审发现搜索页既有 DOM 更新,又有样式计算和输入处理,团队却只检查 JavaScript 回调顺序;你会要求补充什么运行模型?
参考回答

应把 DOM 处理、样式计算、布局、脚本和输入事件统一放进渲染主线程的调度模型中分析。需要结合任务进入队列、主线程取出任务及当前占用情况检查交互链路;只看回调源码会漏掉布局等主线程工作造成的等待。

追问 3一个页面已经接入事件循环,但图表计算期间点击仍无响应;产品据此认为调度系统失效,你如何判断?
参考回答

这更可能是当前任务长期占用主线程,而不是事件循环没有工作。事件循环负责统筹任务,却不能让主线程突破单线程的执行容量;应查看计算、样式或布局是否形成长时间占用,材料不足以给出具体拆分阈值。

追问 4线上表单偶发输入延迟,网络请求已经结束,回调、样式更新和布局却都晚了一拍;排查时你先看服务端还是主线程?
参考回答

应同时核对网络完成时刻与主线程时间线,先区分消息到达晚还是到达后消费晚。若网络已完成而主线程仍在执行脚本、样式或布局工作,延迟来自排队或占用;仅凭用户看到页面晚更新,不能直接归因于服务端或事件循环。

追问 5架构评审有人建议为点击、布局和脚本各建一套互不相关的执行机制,以避免相互影响;基于材料你会接受吗?
参考回答

不能仅凭任务类型就把它们视为彼此独立的主线程执行体系,因为这些页面工作最终仍受统一调度并占用同一主线程。分类队列或优先级可能属于具体实现,但材料没有提供依据;即使细分入口,也无法消除主线程容量和长任务风险。

# 使用单线程处理安排好的任务

30 秒速记

  • 当任务在启动前已经确定,可以把对应代码按目标顺序直接放入主线程。
  • 单线程同一时刻只执行一个任务,示例中的三次计算会依次完成,随后才执行结果打印。
  • 后续任务若依赖前序结果,代码顺序就同时表达了执行次序和数据依赖。
  • 全部任务完成后线程退出,适用于任务集合预先可知且无需持续接收新任务的简单场景。
  • 这种静态安排方式的边界是只能处理已经写入执行流程的任务;材料未给出动态任务进入时的处理方案。

任务在启动前已经确定时,可以按依赖顺序把代码直接放进主线程,让单线程依次执行。 同一时刻它只能处理一项工作,所以会先完成三次计算并保存结果,再执行打印,代码顺序也就表达了执行顺序和数据依赖。入口函数返回、调用栈清空后,如果没有事件循环等常驻机制,线程就会结束。它适合任务固定且短小的场景;用户输入、网络响应等到达时间不确定的任务无法这样静态安排,耗时计算也会持续占用主线程。

我们先从最简单的场景讲起,比如有如下一系列的任务:

  • 任务 1:1+2
  • 任务 2:20/5
  • 任务 3:7*8
  • 任务 4:打印出任务 1、任务 2、任务 3 的运算结果

现在要在一个线程中去执行这些任务,通常我们会这样编写代码

void MainThread(){
     int num1 = 1+2; // 任务 1
     int num2 = 20/5; // 任务 2
     int num3 = 7*8; // 任务 3
     print(" 最终计算的值为:%d,%d,%d",num,num2,num3); // 任务 4
  }
@前端进阶之旅: 代码已经复制到剪贴板

在上面的执行代码中,我们把所有任务代码按照顺序写进主线程里,等线程执行时,这些任务会按照顺序在线程中依次被执行;等所有任务执行完成之后,线程会自动退出。可以参考下图来直观地理解下其执行过程:

原理拆解: 这种模型属于静态编排:任务集合和依赖关系在主线程启动前已经确定,所以源码中的语句顺序可以直接充当调度顺序。线程维护一个调用栈,同一时刻只执行栈顶对应的工作;前三个表达式依次求值并保存结果,打印语句必须等这些赋值完成后才能读取数据。入口函数返回且调用栈清空后,若没有事件循环或其他常驻机制,线程的工作便结束。

具体实现: 用 JavaScript 表达同一过程时,可以依次计算 1 + 2、20 / 5 和 7 * 8,再把三个结果传给 console.log。预期输出为 3 4 56,并且打印不可能插入到任一同步表达式的求值过程中。这里的数据依赖也约束了次序:如果第四项读取前三项的变量,就必须放在赋值之后,否则可能遇到未初始化错误或读到不符合预期的旧值。

边界与反例: 静态顺序只适合已知、短小且能够直接执行的任务。用户输入、网络响应和定时器回调到达时间不确定,无法在启动时全部写成一段固定任务序列;若主线程用耗时循环等待它们,还会阻塞后续工作。即使任务已知,长时间计算也会独占线程,顺序正确并不等于响应及时。

工程验证: 可在每项任务前后调用 console.timeStamp,或用浏览器开发者工具的 Performance 面板录制执行过程。应观察到这些同步计算位于同一个主线程任务内且连续发生,没有其他主线程任务穿插。若时间线上出现长任务,说明应拆分计算或转移工作,而不是改变几条同步语句的排列。

面试官追问

追问 1结算脚本依次计算三项金额再打印汇总,有人把 console.log 挪到第二项计算之前,并称单线程会自动等齐结果;这个改动会怎样?
参考回答

单线程只保证按源码顺序执行,不会自动等待后续赋值,因此打印可能遇到未初始化变量或读到不符合预期的旧值。汇总语句必须位于其依赖的三项计算之后;顺序正确仍不保证计算足够快。

追问 2批处理页面启动时已有三项互不依赖的同步计算,评审想交换前两项顺序以便阅读;这会破坏静态编排模型吗?
参考回答

若两项确实没有数据依赖,交换它们不会破坏单线程串行执行模型,线程只会按照新的源码顺序逐项求值。仍应确认日志、共享状态等隐含副作用;仅凭算式独立,不能推广到所有业务任务都可任意重排。

追问 3报表页从固定的四项计算改为运行中持续接收用户筛选条件,负责人仍想把所有逻辑顺序写进一个入口函数;为什么不够?
参考回答

固定入口函数只能覆盖启动前已经确定的任务集合,无法预先排列到达时间不确定的用户输入。需要常驻的等待与调度机制接收新任务;若用耗时循环主动等待输入,还会占住线程并阻塞后续工作。

追问 4性能录制中,三项计算和一次打印连续出现在同一个主线程任务里,但整段持续很久且期间没有点击处理;你如何定位?
参考回答

这说明同步语句确实按顺序连续执行,但其中至少一段计算可能形成了长任务并独占主线程。可在各项前后加入 console.timeStamp,结合 Performance 时间线定位耗时区间;调整语句顺序不能解决计算总量过大的问题。

追问 5技术负责人要在“保留一段顺序同步代码”和“引入事件循环接收后续任务”之间选型,任务目前全部已知且执行后程序即可退出,你倾向哪种?
参考回答

在任务全部预先确定、依赖清晰且执行时间可控时,顺序同步代码已经能直接表达调度关系,引入常驻循环没有必要。若未来要处理输入、网络或定时器等动态任务,静态编排就会失效;选择简单模型的代价是扩展能力有限。

追问 6新人看到打印不会插入任一表达式求值中,便推断其他主线程事件也能在两条同步语句之间随时穿插;你怎么解释调用栈与任务的关系?
参考回答

这些同步计算位于同一个主线程任务内,会沿调用栈连续执行,其他主线程任务不能任意插进表达式求值过程。只有当前任务结束并交还执行权后,事件循环才有机会选择后续工作;材料没有据此规定更细的任务优先级。

# 在线程运行过程中处理新任务

30 秒速记

  • 线程启动后仍会不断产生新任务,不能只依赖启动前编排好的固定执行序列。
  • 事件循环让线程持续重复“等待事件、接收输入、执行任务”的过程,从而具备动态处理能力。
  • 等待输入时线程会暂停;事件到达后线程恢复运行,完成计算并再次进入等待。
  • 这一模型解决的是线程运行期间接收新事件的问题,示例中的任务仍来自线程自身,尚未覆盖其他线程投递任务。
  • 循环本身不等于高效调度;如果单次任务长期占用线程,后续事件仍无法及时得到处理。

线程运行过程中产生的新任务,需要通过事件循环持续等待、接收并执行,而不能只靠启动前排好的固定顺序。 简单来说,线程在循环中等待用户输入,等待时暂停;输入到达后被激活,完成计算和输出,再回到等待状态。比如连续输入两组数字,线程就能分别执行两次加法。这种示例只处理线程自身接收的事件,还没有涉及其他线程投递任务;如果单个任务长期占用线程,后续事件仍然无法及时处理。

← 编译器和解析器:V8如何执行一段JavaScript代码的Webapi:setTimeout是怎么实现的 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • HTTP

    • 基础篇

      • HTTP的前世今生
      • HTTP是什么
      • HTTP世界全览
      • HTTP分层
      • 键入网址到回车发生什么
      • HTTP报文是什么样子的
      • 理解请求方法
      • URI
      • 响应状态码
      • HTTP有哪些特点
      • HTTP优缺点
      • HTTP的实体数据
      • HTTP传输大文件
      • HTTP的连接管理
      • HTTP的重定向
      • HTTP的Cookie机制
      • HTTP的缓存控制
      • HTTP的代理服务
      • HTTP的缓存代理
    • 高级篇

    • 扩展篇

  • 浏览器相关

  • 计算机基础