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

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

你是不是有过以下困难:

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

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


进程和线程


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


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

M_`MJARLHJGJ}X3(SK)BN5T.png


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,不会阻止解析。
  4. 主线程解析CSS样式,并把CSS样式一一对应到DOM节点上,注意,此时CSS页面还没有生效,只是样式和节点绑定了关系。

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

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

  7. 层分好了并确定了顺序之后,主线程就把这个信息提交给合成线程,然后合成器线程把每个图层栅格化,发送给栅格线程,栅格线程把它们存储在GPU内存内。

  8. 最终,合成线程将栅格化的块合成帧,并通过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 (opens new window)》)


定时器

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

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

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

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


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


这段参考Tasks, microtasks, queues and schedules (opens new window),一位谷歌开发者人员用实例讲述了任务执行顺序,并带有在线Demo,强烈建议过一遍(英语不好就逐句翻译)。

在JS中,主线程的任务叫 宏任务(MacroTask) ,宏任务执行完毕后,立即执行的任务叫 微任务(MicroTask)

宏任务:

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

微任务:

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

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

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

当然 Vue 中的 nextTick 也就属于微任务了.

目录
相关文章
|
1月前
|
Web App开发 前端开发 JavaScript
折腾之王:JavaScript之父Brave浏览器与BAT的诞生
2015年,JavaScript之父Brendan Eich再次创业,推出Brave浏览器和加密货币Basic Attention Token(BAT),旨在颠覆传统广告行业。Brave屏蔽广告、保护隐私,加载速度快;BAT则通过奖励机制让用户、内容创作者和广告主三方受益。尽管面临用户习惯和巨头竞争的挑战,Brave已拥有超4000万月活跃用户,成为全球增长最快的隐私浏览器,引领Web3生态发展。
112 22
折腾之王:JavaScript之父Brave浏览器与BAT的诞生
|
3月前
|
JavaScript 前端开发 数据处理
模板字符串和普通字符串在浏览器和 Node.js 中的性能表现是否一致?
综上所述,模板字符串和普通字符串在浏览器和 Node.js 中的性能表现既有相似之处,也有不同之处。在实际应用中,需要根据具体的场景和性能需求来选择使用哪种字符串处理方式,以达到最佳的性能和开发效率。
124 63
|
3月前
|
移动开发 JavaScript 前端开发
一些处理浏览器兼容性问题的JavaScript库
这些库在处理浏览器兼容性问题方面都有着各自的特点和优势,可以根据具体的需求和项目情况选择合适的库来使用,从而提高代码的兼容性和稳定性,为用户提供更好的体验。同时,随着浏览器技术的不断发展,还需要持续关注和学习新的兼容性解决方案。
131 48
|
3月前
|
JavaScript 前端开发 API
深入理解Node.js事件循环及其在后端开发中的应用
本文旨在揭示Node.js的核心特性之一——事件循环,并探讨其对后端开发实践的深远影响。通过剖析事件循环的工作原理和关键组件,我们不仅能够更好地理解Node.js的非阻塞I/O模型,还能学会如何优化我们的后端应用以提高性能和响应能力。文章将结合实例分析事件循环在处理大量并发请求时的优势,以及如何避免常见的编程陷阱,从而为读者提供从理论到实践的全面指导。
|
3月前
|
算法 开发者
Moment.js库是如何处理不同浏览器的时间戳格式差异的?
总的来说,Moment.js 通过一系列的技术手段和策略,有效地处理了不同浏览器的时间戳格式差异,为开发者提供了一个稳定、可靠且易于使用的时间处理工具。
67 1
|
3月前
|
JSON 移动开发 JavaScript
在浏览器执行js脚本的两种方式
【10月更文挑战第20天】本文介绍了在浏览器中执行HTTP请求的两种方式:`fetch`和`XMLHttpRequest`。`fetch`支持GET和POST请求,返回Promise对象,可以方便地处理异步操作。`XMLHttpRequest`则通过回调函数处理请求结果,适用于需要兼容旧浏览器的场景。文中还提供了具体的代码示例。
在浏览器执行js脚本的两种方式
|
3月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
598 1
|
4月前
|
机器学习/深度学习 自然语言处理 前端开发
前端大模型入门:Transformer.js 和 Xenova-引领浏览器端的机器学习变革
除了调用API接口使用Transformer技术,你是否想过在浏览器中运行大模型?Xenova团队推出的Transformer.js,基于JavaScript,让开发者能在浏览器中本地加载和执行预训练模型,无需依赖服务器。该库利用WebAssembly和WebGPU技术,大幅提升性能,尤其适合隐私保护、离线应用和低延迟交互场景。无论是NLP任务还是实时文本生成,Transformer.js都提供了强大支持,成为构建浏览器AI应用的核心工具。
917 1
|
5月前
|
JavaScript 前端开发
js之浏览器对象|28
js之浏览器对象|28
|
4月前
|
JavaScript API
深入解析JS中的visibilitychange事件:监听浏览器标签间切换的利器
深入解析JS中的visibilitychange事件:监听浏览器标签间切换的利器
244 0