本文主要介绍如下内容:
在浏览器中输入URL并回车后都发生了什么?
让我们从大家最熟悉的这个面试问题引入,先不往下看文章,你能脱口而出的说出答案嘛?如果可以恭喜你,你可以跳过这一小节。如果不可以那就还是看一下吧~
Emmmm,到这里这道题目基本解答完毕,但是却引出了另一个问题,浏览器从服务端拿到数据后做了什么才将网页呈现到我们的显示器上,下面就让我们一起来探索浏览器的秘密吧~~
首先让我们看一下浏览器的主要组件:
值得注意的是,和大多数浏览器不同,Chrome浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程。
在浏览器的主要组件中我们最关注的就是浏览器的呈现引擎,因为呈现引擎,顾名思义,它决定了呈现在浏览器的内容。呈现引擎也叫做浏览器内核,不同浏览器使用的呈现引擎是不一样的。常见浏览器使用的呈现引擎如下:
呈现引擎 浏览器 Trident(MSHTML) IE,MaxThon,TT,The World,360,搜狗浏览器等 Gecko Netscape6 及以上版本,FF,MozillaSuite/SeaMonkey 等 Presto Opera7及以上。 [Opera内核原为:Presto,现为:Blink] Webkit Safari,Chrome等。[Chrome:Blink(WebKit 的分支)] EdgeHTML Microsoft Edge。 [此内核其实是从 MSHTML fork 而来,删掉了几乎所有的 IE私有特性]
下面我们将以Webkit为例讲解浏览器呈现引擎工作的主要流程,Gecko的工作流程与Webkit基本是相同的,只是术语略有不同。
上图展示的是webkit的主要工作流程,接下来我们按照图片的流程来逐渐阐述Webkit是如何工作的。但在这之前我们先要明白从HTTP请求回来开始,呈现引擎的整个工作流程不是一步做完再做下一步,而是一条流水线。
从HTTP请求回来,就产生了流式的数据,后续的DOM树构建、CSS计算、渲染、合成、绘制,都是尽可能地流式处理前一步的产出:即不需要等到上一步骤完全结束,就开始处理上一步的输出,这样我们在浏览网页时,才会看到逐步出现的页面。
Webkit会用HTML解析算法将HTML转换成DOM树。下面让我们看一下HTML到DOM树的转换:
接下来让我们了解一下HTML解析算法,HTML解析算法的流程如下图所示:
它分为标记化和树构建两个过程:
HTML解析完成后,浏览器会将文档状态标注为交互状态,并开始解析那些处于deferred模式的脚本,然后文档状态设置为完成,一个加载时间将随之触发。
CSS解析器会将CSS文件解析成StyleSheet对象,下面让我们来看一下CSS到StyleSheet的转换:
这里我们不讲CSS构建的过程,感兴趣的小伙伴可以看一下参考资料里的重学前端,我们简单的介绍一下CSS选择器的特点,这是由CSS设计原则所决定的。
构建呈现树时,需要计算每一个呈现对象的可视化属性。每个DOM节点都有一个"attach"方法,在节点插入DOM树时会调用节点的attach方法,计算该节点的样式属性生成呈现器。下面让我们看一下整合(webkit的术语叫‘附加’)的过程:
所有的呈现器都有一个“layout”或者“reflow”方法,每一个呈现器都会调用其需要进行布局的子代的layout方法。有很多排版方法:正常流文字排版,绝对定位,浮动元素排版,flex排版等。
这里的渲染是借用计算机图形学里面的解释,就是把模型变成位图的过程。
位图就是在内存里建立一张二维表格,把一张图片的每个像素对应的颜色保存进去(位图信息也是DOM树中占据浏览器内存最多的信息,我们在做内存占用优化时,主要就是考虑这一部分)。
这个过程实际上是一个性能考量,它并非实现浏览器的必要一环。合成的过程就是根据合成策略合并位图。合成策略就是最大限度的减少绘制次数,它是“猜测”可能变化的元素,将它排除到合成之外。
目前,主流浏览器一般根据position、transform等属性来决定合成策略,来“猜测”这些元素未来可能发生变化。但是,这样的猜测准确性有限,所以新的CSS标准中,规定了will-change属性,可以由业务代码来提示浏览器的合成策略,灵活运用这样的特性,可以大大提升合成策略的效果。
一般来说,浏览器并不需要用代码来处理这个过程,浏览器只需要把最终要显示的位图交给操作系统即可。
到这里我们已经将Webkit主要的工作流程捋了一遍,现在让我们来总结一下,从HTTP请求回来的数据通过HTML解析器和CSS解析器,分别解析成DOM树和StyleSheet对象,然后整合两者生成呈现树,呈现树调用layout进行排版,然后通过渲染将呈现器盒子变成位图,根据合成策略合成位图提升绘制性能,把位图给操作系统让其绘制到屏幕上。看到这里我们很容易就理解了一个小知识点:CSS不会阻塞DOM的解析,但会阻塞DOM的渲染。
现在我们已经以Webkit为例介绍了呈现引擎的主要工作流程,但是我们似乎还遗漏了些什么。对的,Javascript,我们好像一直没有提及当Webkit解析到JavaScript代码时会怎么处理,接下来就让我们一起来看一看这一部分知识吧~~
浏览器加载JavaScript脚本,主要通过<script>元素完成。其正常流程如下
加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是 JavaScript 代码可以修改 DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。
浏览器解析到包含defer属性的<script>元素时,其运行流程如下
使用defer属性时需要注意的点:
浏览器解析到包含async属性的<script>元素时,其运行流程如下
使用async属性时需要注意的点:
<script>元素还可以动态生成,生成后再插入页面,从而实现脚本的动态加载。动态生成的script标签不会阻塞页面渲染,也就不会造成浏览器假死。但是问题在于,这种方法无法保证脚本的执行顺序,哪个脚本文件先下载完成,就先执行哪个。如果想避免这个问题,可以设置async属性为false。还可以监听脚本的onload事件来为脚本指定回调。
因为JS脚本可能会引用DOM的样式做计算,所以为了保证脚本计算的正确性,Firefox浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行。
此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。
WebKit和Firefox都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改DOM树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
Emmmm,到这里我们就了解了浏览器的呈现引擎只负责解析HTML和CSS,遇到JS时它会把控制权交给JS的引擎来解析和执行。因为JS引擎拿走了渲染的控制权,所以JS显而易见会阻塞DOM的解析,为了让JS不阻塞DOM的解析浏览器进行了异步加载以及预解析等优化。嗯,我们已经了解了呈现引擎,接下来让我们了解一下它的小伙伴Javascript引擎的工作流程吧~~
首先,让我们了解几个概念,这会帮助我们更好的理解js代码是如何执行的。
了解基础的概念后,现在让我们一起来捋一遍Javascript引擎是如何工作的吧~~
将宿主(例如浏览器)发起的宏观任务添加到宏观任务队列,如果JS引擎主线程的任务栈是空的,它会自动从宏观任务队列拉取任务并执行,在执行过程中遇到setTimeout等异步代码会先放到计时器模块,计时器模块计时结束后加入将其加入宏观任务队列;在执行过程中遇到Promise等代码会将其作为一个微观任务加入到当前宏观任务末尾的微观任务队列中。当前宏观任务正常任务执行完毕后会执行当前宏观任务末尾的微观任务队列里面的任务,微观任务队列内任务执行完毕后该宏观任务执行完毕,主线程任务栈会拉取下一个宏观任务。在此期间宿主环境随时可能在宏观任务队列添加任务,JS引擎也随时可能在当前宏观任务队列末尾的微观任务队列添加微观任务。如图所示,形成了一个事件循环。
Emmmm,如果小伙伴们想进一步的了解JS引擎的工作细节,我推荐以下文章和视频来chrome的V8引擎是如何工作的。
感恩大家还能看到这里,文章可能还有一些地方不是特别完善,我会慢慢迭代完善的~~
最后谈一点点自己的感想,我个人一直觉得学习原理很重要,最近学习了圈外解决问题的课程就更加坚定了我的信念。因为解决问题的第一步就是澄清问题,而学习原理可以帮助我们更快的定位问题所在从而解决问题。爱因斯坦曾说,如果给我一个小时解答一道决定我生死的问题,我会花55分钟来弄清楚这道题目到底是在问什么。一旦清楚它到底在问什么,剩下的5分钟足够回答这个问题。在实际的工作中也确实如此,一旦程序出了问题,我们往往花大量的时间在调试上,而一旦找到了问题解决起来就很快了。
因为前端工程师打交道最多的就是浏览器,了解浏览器的工作原理不管是对写代码还是对项目的性能优化都会有所帮助,所以我断断续续看了许多关于浏览器工作原理的文章及书籍小册,终于觉得是时候整理输出一些东西,希望可以加深自己的理解,更希望可以对小伙伴们有所帮助。如果文章中有什么表述不对的地方,欢迎大家在评论区指正。最后感谢阅读这篇文章的小伙伴们。