一文讲解浏览器运行渲染机制、JS任务队列及事件循环

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 你是不是有过以下困难:• 多个方法互相嵌套,但是最终还是蒙对了• 不是很明白为什么浏览器有时候会卡死• 事件循环好像知道那么点,但是就是讲不出来为啥• ……本篇文章就把你的问题给一一解答,当然这些东西想完弄清楚,肯定离不开进程,线程,浏览器内核,渲染,事件循环,任务队列等,我们就一个一个的来看,它们到底是怎么工作的。

网络异常,图片无法展示
|

你是不是有过以下困难:

  • 多个方法互相嵌套,但是最终还是蒙对了
  • 不是很明白为什么浏览器有时候会卡死
  • 事件循环好像知道那么点,但是就是讲不出来为啥
  • ……

本篇文章就把你的问题给一一解答,当然这些东西想完弄清楚,肯定离不开进程,线程,浏览器内核,渲染,事件循环,任务队列等,我们就一个一个的来看,它们到底是怎么工作的。


进程和线程


举个例子,一个工厂,它有自己独立的资源,工厂和工厂之间相互独立,各自做各自的事情。一个场子可以有很多工人,工人可以 单个作业 也可以 协同作业,工人做的事情,都只会在自己的工厂内,并且共享这个工厂的空间。

我们现在把概念放到进程上,一个进程就相当于一个工厂,工厂里的资源就相当于系统分配的独立内存,多个工厂各自做各自的事情就相当于进程之间相互独立,一个工厂有很多工人就相当于一个进程可以有很多线程,工人的作业就相当于线程完成任务,工人共享这个工厂的空间,就相当于一个进程下面的线程之间可以共享程序的内存。

网络异常,图片无法展示
|

在 windows 的任务管理器中 CPU 和 内存 可以把每个进程的占用看的很清楚,当然 Mac OS 从活动监视器中也可以看到。所以:进程是 cpu 资源分配的最小单位,线程是cpu调度的最小单位。

网络异常,图片无法展示
|
网络异常,图片无法展示
|

大家所说的多线程和单线程,都是只在一个进程内的多和单!


浏览器


构成

前提:页面是跑在浏览器上的,也就是说浏览器是页面的载体,浏览器会制定一套规则,页面满足了这个规则然后才可以在到浏览器上正常运行

浏览器本质上其实是一个软件,它运行在一个操作系统上(windows 或 MacOS 或 其他),一般来说操作系统会开一个端口去运行这个软件,也就是为这个进程分配了CPU,内存 和 磁盘空间等。

那浏览器是单进程还是多进程呢?我们看一下:

网络异常,图片无法展示
|

可见它是个多个进程的浏览器!

在 Chrome 多进程架构里,它包括了四个进程:

  • Browser进程(负责地址栏、书签栏、前进后退、网络请求、文件访问等)
  • Renderer进程(负责一个Tab内所有和网页渲染有关的所有事情,是最核心的进程
  • GPU进程(负责GPU相关的任务)
  • Plugin进程(负责Chrome插件相关的任务)

如果你打开它的任务管理器,你会发现:

网络异常,图片无法展示
|

上图我们可以看出:一个标签页就是一个进程,甚至一个扩展程序就是一个进程!在浏览器中打开一个网页就相当于新开了一个进程。

但是:在这里浏览器有自己的优化机制,有时候打开多个标签页,进程会合并,所以每一个标签页对应一个进程不是绝对的。

这样的多进程分配的好处是:

  • 如果一个页面挂了,不会影响其他页面,甚至影响到整个浏览器
  • 避免安装的三方插件等影响了浏览器全局
  • 多进程充分利用了多核的优势
  • 把插件,扩展程序等全部隔离,提高稳定性

当然,缺点很就明显了,内存消耗大,确实有点像空间换时间的意思。


请求,响应


接下来我们看下浏览器是如何通过输入内容来请求成功的。

  1. 当用户在地址栏输入内容时,UI线程首先问的是“这是搜索查询还是URL?”。在Chrome浏览器中,地址栏也是搜索输入字段,因此UI线程需要解析并决定是将您发送到搜索引擎还是请求的网站。
    网络异常,图片无法展示
    |

  2. 当用户按下Enter键时,UI线程会发起网络调用以获取网站内容。加载微调框显示在选项卡的角上,并且网络线程通过相应的协议(例如DNS查找和为请求建立TLS连接)。
    此时,网络线程可能会收到服务器重定向标头,例如HTTP301。在这种情况下,网络线程与服务器正在请求重定向的UI线程进行通信。然后,将启动另一个URL请求。
    网络异常,图片无法展示
    |

  3. 一旦有响应了,网络线程将在必要时查看流的前几个字节。响应的Content-Type标头应说明它是什么数据类型,但是由于可能丢失或错误, 因此在此处进行MIME Type检查。
    如果响应是HTML文件,则下一步是将数据传递到渲染器进程,但是如果是zip文件或其他文件,则意味着这是下载请求,因此它们需要将数据传递到下载管理器。
    网络异常,图片无法展示
    |

  4. 网络线程从安全站点询问响应数据是否为HTML,并进行安全检查。
    网络异常,图片无法展示
    |

    在这个时候,浏览器已经拿到响应了,接下来就开始进行渲染了。
  5. 一旦完成所有检查,并且Network线程确信浏览器应导航到请求的站点,则Network线程将告知UI线程数据已准备就绪。然后,UI线程找到一个渲染器进程来进行网页渲染。
    网络异常,图片无法展示
    |

  6. 现在已经准备好数据和渲染器进程,将IPC从浏览器进程发送到渲染器进程以提交导航。它还会传递数据流,因此渲染器进程可以继续接收HTML数据。一旦浏览器进程听到确认已在渲染器进程中进行提交的确认,导航即完成,文档加载阶段开始。
    此时,地址栏已更新,安全指示符和站点设置UI反映了新页面的站点信息。选项卡的会话历史记录将被更新,因此后退/前进按钮将逐步浏览刚刚导航到的站点。为了方便在关闭选项卡或窗口时恢复选项卡/会话,会话历史记录存储在磁盘上。
    网络异常,图片无法展示
    |

    到这里为止,浏览器的请求和响应就完成了。那在响应之后如何渲染呢,我们接着往下看


渲染


先说几个渲染进程内将要工作的线程:

  • 主线程(Main thread):下载资源、执行js、计算样式、进行布局、绘制合成
  • 光栅线程(Raster thread)
  • 合成线程(Compositor thread)
  • 工作线程(Worker thread)

在下面的渲染过程中,其实就是这四个进程的互相配合,我们一起来看下吧。

  1. 当渲染过程接收提交消息用于导航和开始接收HTML数据,主线程开始解析文本串(HTML),使之成为一个 Document Object Model ,也就是 DOM
  2. 网站有用到图片,CSS 和JavaScript的话,这些东西需要从网络或者缓存中加载,主线程可以边请求,边预加载构建DOM。
    网络异常,图片无法展示
    |

  3. 当HTML解析器找到 <script> 标签后,将会暂停HTML解析,并且必须加载、解析和执行 JavaScript的代码。为什么?因为JavaScript 可以使用诸如 document.write() 更改整个DOM结构!所以开发人员在写代码的时候可以在 <script> 标签上加 async 或者 defer 属性。然后浏览器将会异步加载并运行JavaScript,不会阻止解析。
  1. 主线程解析CSS样式,并把CSS样式一一对应到DOM节点上,注意,此时CSS页面还没有生效,只是样式和节点绑定了关系。
    网络异常,图片无法展示
    |

  2. 接下来CSS根据DOM节点,会生成类似于DOM结构的一个布局树,仅包含了页面上可见内容的信息,如果有 display: none 等,则该元素不属于布局树。如果有 p::before {content:"123"} 等伪类的存在,就算它不在DOM中,也会包含在布局树中。
    网络异常,图片无法展示
    |

    在此绘制步骤中,主线程遍历布局树以创建绘制记录。绘画记录是绘画过程的注释,例如“先是背景,然后是文本,然后是矩形”,类似 canvas
    注意:在渲染的时候,每个步骤前面操作的结果都用于创建新数据,如果布局树发生了更改,文档受影响的部分就会重新绘制,也就是 重绘,开发过程中要尽量避免这一现象。
  3. 至此浏览器知道了:文档的结构,每个DOM元素的样式,页面的几何形状以及绘制的顺序。把这些东西换转为屏幕上像素我们称之为 光栅化。在现代浏览器中执行这一行为的过程,称为 合成(Compositing),就是把页面各个部分分成若干层,分别进行栅格化,然后合成器线程的单独线程中进行合成,一个层可以称之为一个 layer。
    网络异常,图片无法展示
    |

  4. 层分好了并确定了顺序之后,主线程就把这个信息提交给合成线程,然后合成器线程把每个图层栅格化,发送给栅格线程,栅格线程把它们存储在GPU内存内。
    网络异常,图片无法展示
    |

  5. 最终,合成线程将栅格化的块合成帧,并通过IPC传递给浏览器进程,显示在屏幕上。
    网络异常,图片无法展示
    |

至此,浏览器的请求,响应和渲染过程结束!

(一半了,稍微休息一下,我们再继续!)


JS单线程


回顾一下,浏览器的渲染进程中,主线程里包括了执行JS,那也就意味着:JS在浏览器的 渲染进程(Rendered Process) 的 主线程(Main Thread) 内!

网络异常,图片无法展示
|

记住:JS是被设计成单线程的!

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?—— 阮一峰

所以叙述出来就是:JS逻辑 和 UI渲染 是在一个线程中顺序发生的,二者同一时间只可以存在一个。继续回顾一下上面渲染所提到的,HTML解析器必须等待JS运行,JS是可以操作DOM 和 布局树的,会干扰到主线程在解析HTML的顺序,从而影响结果,所以为了页面的渲染统一,JS被设计成了 执行阻塞UI渲染型。

同时也反映出了一个问题:JS过多会造成页面卡顿,因为走不下去了。所以JS的逻辑一定不能冗余。


任务队列


既然JS是单线程,也就意味着里面的逻辑是排队运行的,后一个任务必须等前一个结束才可以运行。这样就会出一个问题,有没有一种可能是挂起不那么重要的任务,先走重要的,等结束之后再执行挂起的任务呢?

按照这个说法的话,所有任务就可以分成:同步任务(sync)异步任务(async) ,同步任务就是主线程里面的排队进行,异步任务就是不进入主线程,进入一个 “任务队列(task queue)” 的地方呆着,看着主线程里的任务进行,一旦发现主线程的同步任务执行完了,就通知主线程,说我这里的异步任务可以执行了,该任务才会进入主线程执行。

所以有没有发现,那些鼠标点击事件,页面滚动,回调函数,http请求……其实就在任务队列里面。


事件循环(Event Loop)


概念

有了任务队列的存在,就会有事件循环的存在,因为任务队列中可能有很多任务,一个在任务队列的任务进入到主线程后,任务队列依然会看着主线程,看看刚进去的这个有没有执行完毕,毕竟任务队列里还有很多没执行的任务,所以主线程去读取任务队列是循环不断的,也就叫做了 事件循环

这里放张网图,基本上一看就明白了(参考自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)

网络异常,图片无法展示
|


定时器

这个有点特殊,单独讲一下。

定时器不是个异步事件,是一个定时事件,但是仍属于一个回调操作,是被放在任务队列中的。

就算定时器被设置的时间是0,它也仍然会在主线程逻辑走完之后(此时栈清空了),再执行,所以时间是0的定时器,它可以被理解为希望尽早的执行。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。——阮一峰


微任务(MicroTask)和宏任务(MacroTask)

这段参考Tasks, microtasks, queues and schedules,谷歌开发者人员用实例讲述了任务执行顺序,并带有在线Demo,强烈建议过一遍(英语不好就逐句翻译)。 在JS中,主线程的任务叫 宏任务(MacroTask) ,宏任务执行完毕后,立即执行的任务叫 微任务(MicroTask)

宏任务:

  • 主线程已经存在了的任务叫宏任务,从任务队列中进入主线程的任务也叫宏任务,一个宏任务执行过程中,从头到尾不会执行其他的东西
  • 浏览器会在一个宏任务结束后,在下一个宏任务开始前,对页面进行重新渲染

微任务:

  • 当前宏任务执行结束后立即执行的任务叫微任务,也就是说它在前宏任务之后,后宏任务之前,渲染之前!
  • 它的速度比定时器要快,因为不用等待渲染,定时器是宏任务
  • 在一个宏任务执行结束后,所有的微任务都会执行完毕(渲染前)

基于上面的概念,我们可以给常用的任务分下类:

  • 宏任务:主代码,setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O,UI渲染
  • 微任务:Promise,process.nextTick,MutationObserve,queueMicrotask

当然 Vue 中的 nextTick 也就属于微任务了,最后放一张图帮助一下理解:

网络异常,图片无法展示
|


目录
相关文章
|
30天前
|
JavaScript 前端开发 Go
CSS 与 JS 对 DOM 解析和渲染的影响
【10月更文挑战第16天】CSS 和 JS 会在一定程度上影响 DOM 解析和渲染,了解它们之间的相互作用以及采取适当的优化措施是非常重要的。通过合理的布局和加载策略,可以提高网页的性能和用户体验,确保页面能够快速、流畅地呈现给用户。在实际开发中,要根据具体情况进行权衡和调整,以达到最佳的效果。
|
1月前
|
存储 JavaScript 前端开发
深入理解JavaScript中的事件循环(Event Loop):机制与实现
【10月更文挑战第12天】深入理解JavaScript中的事件循环(Event Loop):机制与实现
76 3
|
2月前
|
JavaScript 前端开发 API
详解队列在前端的应用,深剖JS中的事件循环Eventloop,再了解微任务和宏任务
该文章详细讲解了队列数据结构在前端开发中的应用,并深入探讨了JavaScript的事件循环机制,区分了宏任务和微任务的执行顺序及其对前端性能的影响。
|
14天前
|
数据采集 JavaScript 搜索推荐
服务器端渲染(SSR)(Nuxt+Next.js)
服务器端渲染(SSR)技术在服务器上生成页面HTML,提升首屏加载速度和SEO效果。Nuxt.js和Next.js分别是基于Vue.js和React.js的流行SSR框架。Nuxt.js提供自动化路由管理、页面级数据获取和布局系统,支持SSR和静态站点生成。Next.js支持SSR、静态生成和文件系统路由,通过`getServerSideProps`和`getStaticProps`实现数据获取。SSR的优点包括首屏加载快、SEO友好和适合复杂页面,但也会增加服务器压力、开发限制和调试难度。选择框架时,可根据项目需求和技术栈决定使用Nuxt.js或Next.js。
|
20天前
|
JavaScript 前端开发 开发者
JavaScript的事件循环
【10月更文挑战第27天】理解JavaScript的事件循环机制对于正确编写和理解JavaScript中的异步代码至关重要,它是JavaScript能够高效处理各种异步任务的关键所在。
31 1
|
1月前
|
前端开发 JavaScript
深入理解JavaScript中的事件循环(Event Loop):从原理到实践
【10月更文挑战第12天】 深入理解JavaScript中的事件循环(Event Loop):从原理到实践
36 1
|
1月前
|
Web App开发 JavaScript 前端开发
深入理解Node.js事件循环和异步编程模型
【10月更文挑战第9天】在JavaScript和Node.js中,事件循环和异步编程是实现高性能并发处理的基石。本文通过浅显易懂的语言和实际代码示例,带你一探究竟,了解事件循环的工作原理及其对Node.js异步编程的影响。从基础概念到实际应用,我们将一步步解锁Node.js背后的魔法,让你的后端开发技能更上一层楼!
|
1月前
|
Web App开发 前端开发 JavaScript
JavaScript动态渲染页面爬取——Selenium的使用(一)
JavaScript动态渲染页面爬取——Selenium的使用(一)
|
1月前
|
Web App开发 数据采集 JavaScript
JavaScript动态渲染页面爬取——Selenium的使用(二)
JavaScript动态渲染页面爬取——Selenium的使用(二)
|
1月前
|
设计模式 JavaScript API
Node.js 事件循环
10月更文挑战第3天
30 0
Node.js 事件循环