1. 从输入 url 到页面展示都发生了什么?
- 域名解析
- 发起请求
- HTML解析
- CSS解析
- 布局
- 绘制
1.1. 域名解析
在计算机⽹络中,我们只能通过
IP
地址访问到具体的主机。我们不能通过域名直接访问。我们的前端的静态资源等,都是存储在服务器上。当输⼊⼀个域名的时候,我们⾸先要做的就是将域名转化成IP地址。在转换的过程中,有以下⼏个步骤:
⾸先浏览器会查询⾃身的缓存中,有没有此条域名的解析,如果有的话,就返回这个解析后的地址。
如果浏览器⾃身的缓存中,没有找到与此条域名对应的
IP
地址,那么就会去操作系统中的缓存中查找是否有这条域名的解析。如果在操作系统中也没有找到的话,那么就需要通过
DNS
(域名系统)帮助我们解析。如果浏览器在⾃身缓存中,以及操作系统的缓存中,并未命中该域名的匹配的话,那么就会查找
TCP/IP
参数中设置的⾸选DNS服务器,我们把他叫做本地DNS
,如果本地DNS
服务器的缓存中命中了该域名,就返回该域名的解析。如果解析不到,那么会根据本地DNS
服务器的设置,看是否设置了转发模式,如果设置了转发模式,那么他就会⼀级⼀级的去查找,直到找到。如果还没有找到的话,并且这个时候,DNS
服务器已经没有启⽤转发模式,那么就会向根DNS
服务器发起查询请求。当向
DNS
根服务器发起请求的时候,根服务器会返回当前他所已知的顶层域名服务器,然后,接着向这些顶层域名服务器去发起请求,如果某个顶层域名服务器解析,是属于他所管辖的范畴,那么就会返回他所管辖的⼆级DNS
域名服务器,以此类推,直到找到或者找不到。
DNS
的全称是domain name system
,也就是域名系统。他⼯作在应⽤层,主要作⽤是帮我们完成域名到IP
的转化。他的体系结构是 分布式集群 ,顶层为 根服务器 ,接下来是 顶层域名服务器 ,接下来是 次级域名服务器 。
1.2. 发起请求
当域名解析完毕之后,就会发起请求。我们在这⾥假定这个域名从来没有被访问过。那么它会经过以下⼏个阶段:如果是第⼀次请求,那么在请求后,收到的响应中,会有⼀些关于强弱缓存的字段,⽐如:
强缓存
expires
cache-control
弱缓存
Last-Modified & If-Modified-Since
Etag & If-None-Match
1.3. HTML解析
在请求到资源之后,浏览器需要解析
HTML
,⽣成dom
树,cssRule
树。结合之后形成render
树,之后再渲染。浏览器在解析
HTML
的时候,主要所做的事情是两个:词法分析和语法分析。
词法分析
所谓的词法分析就是将⼀⼤段字符串转根据规则解析成⼀个个最⼩有意义的单元,之后再根据这个最⼩意义单元的相应数据⽣成⼀个
token
。词法分析阶段采⽤的算法是:标记化算法(将
html
从左到右依次的读⼊字符,内部使⽤状态机来断⾔当前的状态,根据语法规则匹配出可以分解的htmlToken
,最后将这个htmlToken
提供给语法分析阶段)
语法分析
语法分析的作⽤是根据词法分析阶段⽣成的htmlToken,将其转化成⼀颗树状结构,也就是我们所说的dom树。在将这些分好的词转化成dom树的时候,我们需要⽤到⼀种数据结构:栈。
在开始之前,先向栈顶压⼊根元素,等到解析完成之后,这个根元素就是最后的dom树。当解析完⽣成⼀个词的时候,就会将他⼊栈,有以下⼏种操作的可能:
- 如果是⼀个开始节点的话,那么直接⼊栈。不做任何操作
- 如果前⼀个是⽂本节点,并且本次⼊栈的也是⽂本节点的话,会将最后⼊栈的⽂本节点与前⼀个⽂本节点进⾏合并。先把它添加到当前栈顶元素的⼦节点数组中,然后⼊栈。
- 如果是注释节点,那么直接添加到当前栈顶元素的⾃⼰诶单数组中。
- 如果是属性的话,直接添加到当前栈顶元素的属性中。
- 遇到⼀个结束节点,就向前找到第⼀个与之匹配的开始节点,并且出栈。
此处如果 header
中同步存在 js
脚本解析,会阻止 GUI
渲染线程。
1.4. CSS解析
- 在⽣成
dom
树的时候,也会解析css
,这两个是并⾏执⾏的,⼀旦存在css
样式(包括但不限于⾏内样式,外部样式引⼊等),就会根据语法规范进⾏解析和标记。解析完成后,会⽣成⼀个stylesheet
对象,这个对象⾥⾯包含着解析好的css
规则,css
规则是由选择器和声明对象组成。
1.5. 布局
render树的⽣成
等到css的rule树与dom树都解析完毕之后,那么就会根据这两个树⽣成最终的render树。
render树的⽣成,就是遍历当前⽣成的dom树,根据当前的dom树的⼦节点信息以及对应的css规则,最终⽣成⼀个或多个render⼦节点。
遍历render树,根据render节点的类型,确定元素的⼤⼩以及位置。
1.6. 绘制
在绘制阶段,系统会遍历render树,并调render树的⼦节点的“paint”⽅法,将render树的⼦节点的内容显示在屏幕上。绘制⼯作是使⽤⽤户界⾯基础组件完成的。
CSS2 规范定义了绘制流程的顺序。绘制的顺序其实就是元素进⼊堆栈样式上下⽂的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。块呈现器的堆栈顺序如下:
- 背景颜⾊
- 背景图⽚
- 边框
- ⼦代
- 轮廓
2. 浏览器的进程与线程
2.1. 在浏览器中,进程主要有以下几个:
GPU
进程。浏览器全局只有这么一个进程。主要是与图形渲染有关。其他插件的进程,比如你给你浏览器装了一个插件,那么这一个插件就是一个进程。
Browser
进程:浏览器的主进程(负责协调、主控),只有一个。主要功能有以下:负责浏览器界面显示,与用户交互。如前进,后退等
负责各个页面的管理,创建和销毁其他进程
浏览器渲染进程,也被称为浏览器内核。分类有:
Google Chrom: Chrome 28开发版本的版本说明中还在使用WebKit,而从Chrome 28.0.1469.0后已经替换为Blink。
**Internet Explorer: Trident内核,也是俗称的IE内核
Mozilla Firefox: Gecko内核,俗称Firefox内核。
Safari: WebKit
Opera: 最初是自己的Presto内核,后来是Webkit,现在是Blink内核
2.2. 浏览器渲染进程
在以上进程中,我们最需要关心的进程是: 浏览器渲染进程,他的主要作用有:负责页面的渲染,脚本执行,时间处理,网络请求等功能。
在一个进程中,至少有一个线程。线程被称为CPU任务调度的最小执行单位。那么在浏览器的渲染进程中,有以下几个线程:
GUI渲染线程: 解析html文档,生成DOM树与CSS树(需要注意的是css树不会阻塞dom树的生成)。当生成DOM树与CSS树之后,就根据这两 个数生成一个render树(在生成render树的时候,如果有一方没有解析完毕就会等待解析完成。此时此刻是双方会互相阻塞),然后将这个render树渲染到界面上。
JS线程:用来执行JS代码。具体执行过程详见JS运行过程详解: https://juejin.cn/post/6950114500289036301
定时器线程:用来处理定时器线程,当定时器到期的时候,将回调放到任务队列里面,等待JS线程的执行。那么有了JS线程,我们为啥还需要定时器线程呢?看下面代码解释:
function test() { setTimeout(() => { console.log('我是计时器1'); }, 1000); setTimeout(() => { console.log('我是计时器2'); }, 2000); }; test();
假如没有定时器线程,又因为JS是单线程的,我只能一个一个的压入栈中执行,那么首先是计时器1入栈,接着是计时器2入栈。但是因为计时器1的时间小于计时器2的时间,那么应该计时器1首先出栈。但是因为栈是一个先进后出的数据结构。那么这就与栈的定义发生了冲突。因为计时器2还没有到时间,没有出栈,所以计时器1也就不能出栈。
事件触发线程。用来管理事件的触发,例如:点击事件,鼠标移动事件。当这些个事件被触发的时候,就会将这些事件的回调添加到任务队列里,等待JS执行。
异步HTTP请求线程。在XMLHttpRequest在连接后新启动的一个线程,线程如果检测到请求的状态变更,如果设置有回调函数,该线程会把回调函数添加到事件队列,同理,等待JS引擎空闲了执行。
2.2.1. JS线程与GUI线程互斥的原因
主要是因为当一个文档在加载的时候,如果此时JS线程也在加载执行,例如要获取一个id为demo1的节点,此时此刻我们渲染过程中,还并没有生成render树,也就不会进行布局和渲染。那么此时这个节点是没有的,那么肯定就会找不到。因此为了避免这种情况的发生,在浏览器中,JS线层和GUI线程是互斥的,当一个执行的时候,另外一个就会被强制挂起。这样就会导致一个问题,当JS执行一个时间复杂度非常高的算法的时候,因为迟迟不能执行完毕,导致GUI渲染线程被挂起太久,就会导致页面看起来卡顿,事件响应变慢。解决办法,可以通过Web Worker解决。