深度帖:浏览器的事件循环与JS异步

一、浏览器进程

早期的浏览器是单进程的,所有功能杂糅在一个进程中;现在的浏览器是多进程的,包含浏览器进程、网络进程、渲染进程等等,每个进程负责的工作不同。

  • 浏览器进程:负责界面显示(地址栏、书签、历史记录)、窗口管理、标签页的创建和销毁、用户交互。
  • 网络进程:负责加载网络资源,HTTP请求等。
  • 渲染进程:负责执行HMTL、CSS、JS代码。每一个页面都会有一个或多个独立的渲染进程。

#浏览器对象模型(Browser Object Module, BOM)交互主要依赖浏览器进程。文档对象模型(Document Object Module ,DOM)主要依赖渲染进程。

还有一些其他的辅助进程,如GPU进程等。

在浏览器"自定义及控制"➡️"更多工具"➡️"任务管理器"查看浏览器进程情况:

二、渲染进程的渲染主线程和消息队列

渲染主线程

前端工程师编写的HTML格式文件代码由渲染进程负责解析最终绘制在页面上。

在这个过程中,渲染主线程是最繁忙的线程,其需要处理任务包括:

  • 解析HTML
  • 解析CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒渲染页面60次(60帧)
  • 执行全局JS代码
  • 执行封装成任务的事件处理和计时器回调函数等

渲染主线程的任务多、任务频繁,其依赖消息队列(message queue,或事件队列)机制,先进先出原则调度任务。

任务源来自:1️⃣渲染主线程正在执行的任务产生的新任务,如JS代码运行产生的各种 2️⃣其他线程向消息队列递交的新任务,如网络请求、用户交互等。

消息队列的演变:宏/微任务队列➡️多任务队列

本章节关于事件循环中队列和队列优先级的内容。

浏览器的任务没有优先级,但消息队列有优先级。

传统将消息队列简单分为宏任务队列和微任务队列。

但浏览器逐渐复杂,在最新W3C标准下,浏览器不再有宏队列的说法,每个任务都有一个任务类型,在一次事件循环中,由浏览器自行决定哪一个队列的任务(浏览器真实的使用环境是复杂多变的)。但浏览器必须包含一个微队列,微队列的任务一定具有最高优先级,必须优先调度。

在目前Chrome的实现中,与前端开发最相关的队列,至少包含了以下几个:

  • 延时队列:用于存放计时器到时后的回调函数「中」。(setTimeOut、setInternel)
  • 交互队列:用于存放用户操作后产生的时间处理函数「高」。(addEventListener)
  • 微队列:用于存放需要最快执行的任务,优先级「最高」。(Promise.then)

面经总结

Q:为什么JavaScript是异步的?

A:从渲染主线程、同步的劣势和异步操作过程等角度回答。

  1. JS的运行环境JS是单线程的语言,这是因为它运行在浏览器渲染进程中的渲染主线程,渲染主线程只有一个。渲染主线程是浏览器线程里最繁忙的一个,承担了许多工作,*解析HTML、解析CSS、计算样式、布局、处理图层、每秒渲染页面60次(60帧)、执行全局JS代码、封装成任务的事件处理和计时器回调函数(选几个回答)*等都在其中执行。
  2. 如果使用同步的方式,极有可能会造成渲染主线程的阻塞,从而导致消息队列中的很多其他任务无法执行。这样一方面会导致主线程阻塞等待白白消耗时间,另一方面导致页面无法及时更新,给用户造成页面卡死现象。
  3. 所以浏览器采用异步的方式,具体做法是当某些任务发生时,比如计时器、网络请求、事件监听,主线程将任务分发交给其他线程去处理,自身立即结束该任务的执行,转而执行后续代码,当被转发任务的线程完成时,将事先传递的回调函数包装成任务加入到消息队列的末尾排队,等待主线程调度执行。在这种异步的模式下,浏览器用不阻塞,从而最大限度保证了单线程的流程运行。

Q:如何理解JavaScript中的事件循环?

A:事件循环也叫消息循环,是渲染主线程的工作方式。它帮助渲染主线程从不同优先级的队列中循环调度任务执行。传统将消息队列简单分为宏任务队列和微任务队列。但浏览器逐渐复杂,在最新W3C标准下,浏览器不再有宏队列的说法,每个任务都有一个任务类型,在一次事件循环中,由浏览器自行决定哪一个队列的任务(浏览器真实的使用环境是复杂多变的)。但浏览器必须包含一个微队列,微队列的任务一定具有最高优先级,必须优先调度。

练习题,阅读代码,写出控制台输出字母顺序:

答案:

第一题:2 1

第二题:3 2 1

第三题:5 4 3 1 2