浏览器原理浅析
前言
本文从浏览器输入url开始到离开页面,来讲解浏览器的原理
主要顺序如下
- 输入解析url
- 获取静态文件(TCP三次握手和四次挥手)
- 渲染页面(html\css\浏览器运行机制)
- 执行js(浏览器线程与进程、事件循环)
- 垃圾回收机制
- 浏览器的几个节点事件
- 从浏览器原理角度看优化
一、浏览器输入url后做了什么?
- url解析
- 查看是否使用本地缓存(作者的另一篇博客),如果用缓存则忽略3-8步
- DNS(全:Domain Name System,中:域名系统)解析
- 建立TCP连接(三次握手)
- 客户端发起HTTP请求
- 服务端响应HTTP请求
- 浏览器接收响应
- 数据传输完成,断开TCP连接(四次挥手),如果Connection:Keep-Alive则不断开
- 客户端渲染页面
url解析
将url解析为:协议、域名、资源路径
如解析https://blog.csdn.net/qq_38217940/article/details/125349105
协议:https
域名:blog.csdn.net
资源路径:/qq_38217940/article/details/125349105(访问页面的时候,路径下面是一个index.html)
DNS解析
步骤如下:
- 查看浏览器缓存有没DNS信息
- 查看主机缓存有没DNS信息
- 查看hosts文件有没修改(hosts文件是可以把ip改成域名的,但是一般人不会改它)
- 查看路由器缓存有没DNS信息
- 从ISP(互联网服务提供商,比如阿里云)中查询DNS缓存
- DNS递归查询
DNS递归查询,如解析https://blog.csdn.net/qq_38217940/article/details/125349105,步骤如下
(1)根服务器(根服务器是什么,这里不介绍,有兴趣自己去查一下)查询.net后缀的服务器
(2)根据后缀.net的服务器查询域名为blog.csdn的服务器
(3) 到blog.csdn上查询返回确定的一个ip如101.201.178.55,一个域名可能有多个ip(负载均衡)
(4) 浏览器访问101.201.178.55
TCP三次握手和四次挥手
简单了解几个名词解释:
- TCP 和 UDP
- 用户数据报协议 UDP(User Datagram Protocol):
UDP 在传送数据之前不需要先建立连接,远程主机在收到 UDP 报文后,不需要给出任何确认。一般用于即时通),比如:语音、 视频 、直播等等 - 传输控制协议 TCP(Transmission Control Protocol):
TCP 提供 面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。
TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。
- TCP报文的几个重要控制位
SYN 同步控制位
ACK 确认控制位
FIN 终止控制位
前置条件,每次只能发一种信号,用于理解握手至少三次,挥手至少四次
三次握手
- 浏览器向服务发送请求连接SYN报文(浏览器握服务器)
- 服务器接受连接后回复 ACK 报文,并为这次连接分配资源(服务器握浏览器)
- 浏览器接收到 ACK 报文后也向 服务器发生 ACK 报文,并分配资源(浏览器握服务器)
为什么是三次不是两次?
为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
栗子:
假如我到你家作客,两次握手:
我:我可以到你家玩吗?
你:可以的。
…30分钟后
我:我开飞机来的,你家怎么没机场?
三次握手:
我:我可以到你家玩吗?
你:可以的。
我:我开飞机过去,你家有机场可以停吗?
你:我出门都是御剑的,没有机场,你别来了。
为什么不是四次或者更多?因为没必要
四次挥手
- 浏览器发送一个FIN(浏览器挥手),用来关闭浏览器到服务器的数据传送,浏览器进入FIN_WAIT_1状态。
- 服务器收到FIN后,发送一个ACK给浏览器(服务器挥手),服务器进入CLOSE_WAIT状态。
- 服务器发送一个FIN(服务器挥手),用来关闭服务器到浏览器的数据传送,服务器进入LAST_ACK状态。
- 浏览器收到FIN后,浏览器进入TIME_WAIT状态,发送ACK给服务器(浏览器挥手),服务器进入CLOSED状态。
为什么是四次挥手?
FIN跟ACK不同时触发,中间确认一次是为了确保是否有数据正在传输,确保正在传输的数据传输完。
栗子:
你:我走了
我:好的,我看看你有没有东西没带走。
我:没啥东西落下,你走吧。
你:好的,我走了
两次挥手的情况:
你:我走了
我:你走吧
…你飞机没开走
三次挥手的情况:
你:我走了
我:你走吧
你:再见
…你飞机没开走
浏览器渲染机制
- 解析HTML,构建DOM树
- 解析CSS,构建CSSOM树
- 将DOM树和CSSOM树合并成为渲染树(render tree)
- 计算渲染树中每个节点的位置
- 通过显卡/GPU绘制页面
重绘
重绘: 当渲染树中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的操作,比如 background-color,我们将这样的操作称为重绘。
回流(重排)
当渲染树中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建的操作,会影响到布局的操作,这样的操作我们称为回流。
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流。
- 添加或者删除可见的 DOM 元素(append、removeChild等)
- 元素尺寸改变——margin、padding、border、width和height
- 内容变化,比如用户在 input 框中输入文字
- 浏览器窗口尺寸改变——resize事件发生时
- 计算 offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
- 修改网页的默认字体。
避免回流
回流必定触发重绘,重绘不一定回流,回流的性能消耗比较大,所以要避免回流
- 使用documentFragment或div等元素进行缓存操作,先把所有要添加到元素添加到1个div,最后才把这个div append到body中。如下
var ul = document.createElement('ul');
var fragment = document.createDocumentFragment();
for(var i=1; i<101;i++){
var li = document.createElement('li')
var liText = document.createTextNode(i);
li.appendChild(liText);
fragment.appendChild(li);
}
ul.appendChild(flag);
document.body.appendChild(ul);
// 创建div的也一样道理,不过就得多加一层div了
let div = document.createElement('div')
- 先display:none 隐藏元素,然后对该元素进行所有的操作,最后再显示该元素。因对display:none的元素进行操作不会引起回流、重绘。
- 将引起回流的属性(offsetWidth 、offsetHeigh)赋值给变量,进行缓存,需要用到的时候直接使用变量就行。
- 对于复杂动画效果,使用绝对定位(postition:absolute)让其脱离文档流
阻塞加载
- 当我们浏览器获得HTML文件后,会自上而下的加载,并在加载过程中进行解析和渲染。
- 加载说的就是获取资源文件的过程,如果在加载过程中遇到外部CSS文件和图片,浏览器会另外发送一个请求,去获取CSS文件和相应的图片,这个请求是异步的,并不会影响HTML文件的加载。
- 但是如果遇到Javascript文件,HTML文件会挂起渲染的进程,等待JavaScript文件加载完毕后,再继续进行渲染。
为什么HTML需要等待JavaScript呢?因为JavaScript可能会修改DOM,导致后续HTML资源白白加载,所以HTML必须等待JavaScript文件加载完毕后,再继续渲染,这也就是为什么JavaScript文件在写在底部body标签前的原因。
浏览器进程跟线程
进程的概念
- 进程是CPU资源分配的最小单位(是能拥有资源和独立运行的最小单位,进程之间不会共享资源),每个APP都至少一个进程,如浏览器、QQ、微信
- 线程是CPU调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程,多个线程之间共享进程的资源)
- 不同进程之间也可以通信,但是代价会比较大
浏览器是多进程的,每一个页签都是单独的进程,每一个页签都包含以下进程:
- 浏览器进程(Browser进程):主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程(多线程):核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程:一开始时为了实现 3D CSS 的效果,随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制
- 网络进程:主要负责页面的网络资源加载
- 插件进程:主要是负责插件的运行
- 音频进程
浏览器的线程,指的是渲染进程,它包括
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 定时触发器线程
- 异步HTTP请求线程(IO线程)
js单线程特点:
- js线程跟GUI渲染线程互斥,所以js执行时,渲染线程时pendding的
- 虽然事件触发、定时器、.异步HTTP请求都是独立的线程,但是js是单线程,所以要都得等js执行完
使用Web Workers开启js多线程,可以用于处理会阻塞js线程的事务,提升性能。具体看这个吧。
事件循环
机制:js是单线程的,按照代码顺序执行,执行完之后看微队列,有微任务则执行微任务,再看宏队列,有宏任务则执行宏任务。
看下面三个问题:
(1)假如js执行过程中有定时器到时间了,是先执行定时器还是先执行完代码?
(2)点击事件的代码执行过程中,定时器时间到了,是先执行定时器还是先执行完点击事件里面的代码?
(3)假如js执行过程中点击事件触发了,是先执行点击事件还是继续执行代码?
不用怀疑,只要js在执行,不管什么事件,不管什么任务,都不能打断它,
实际上js执行的时候页面是属于一个卡住的状态,压根就不存在代码正在执行还能触发点击事件的情况,只不过通常我们不会故意写代码把页面卡住(除非写得很烂)。不信f12打开下面的代码,看看还能不能点击得动页面
for(var i=0;i< 1000000000, i++){
console.log(1)
}
宏任务与微任务
-
宏任务跟微任务是什么?简单来说就是异步任务。
-
宏任务包括
setTimeout、setInterval、setImmediate (Node独有)、ajax
requestAnimationFrame (浏览器独有)、I/O、UI rendering (页面渲染) -
微任务包括
promise、async、await -
宏任务跟微任务的区别(作者个人看法,读者自行分辨):
(1)宏任务包含的内容都是在单独的线程上的,而微任务都是在js线程上的
(2)两个线程消耗的性能肯定比一个线程消耗的多,所以微任务性能消耗会小一点 -
执行顺序
宏任务跟微任务都是先入先出,主事件循环结束后,先执行微任务再执行宏任务。
具体理解看代码
console.log('1');
setTimeout(function() {
console.log('10');
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
},10)
setTimeout(function() {
console.log('7');
new Promise(function(resolve) {
console.log('8');
resolve();
}).then(function() {
console.log('9')
})
})
var a = new Promise(function(resolve) {
console.log('2');
resolve();
}).then(function() {
console.log('5')
})
var b = new Promise(function(resolve) {
console.log('3');
resolve();
}).then(function() {
console.log('6')
})
cosonle.log('4')
// 输出1,2,3,4,5,6,7,8,9,10,11,12
// async是立即执行,没啥好说的
// await按照Promise来理解,里面的相当于resolve之前,函数后为then,比如把a改成下面这样,执行顺序不变,这里就不多介绍了
async function a(){
// 这里要立即执行
await function(){
console.log('2')
}()
console.log('5');
}
a();
事件流
背景小知识
事件冒泡和事件捕获分别由微软和网景公司提出,目的在于解决页面中的事件流问题,即元素间事件触发的时序。当然,分久必合,在微软和网景之间火热争论之后,最后采用了 W3C 的折中方案——先捕获后冒泡。
名词解释
- 事件:事件是指文档和浏览器窗口发生特定交互的瞬间。
- 事件流: 事件流指页面中接受事件的顺序
- 事件捕获:由上而下,根元素最早接受事件,目标元素最后接受事件。
- 事件冒泡:由下而上,目标元素最早接受事件,逐级向上,最后根元素接受事件。
一般情况下 js默认执行事件冒泡(说的是addEventListener第三个参数默认是false),当addEventListener第三个参数是true的时候就是监听的事件捕获。
事件流分为以下三个阶段
(1)事件捕获
(2)目标接受事件
(3)事件冒泡
事件委托
也叫事件代理。利用事件冒泡原理,将子级触发的事件绑定在父级身上。
事件委托优点:
(1)减少多次绑定,提高程序性能
(2)动态添加的子元素也能自动获取事件
<ul id='list'>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
const ul = document.querySelector('#list')
ul.addEventListener('click',function(e){
const target = e.target
if(target.nodeName === 'li'){
console.log(target.nodeName)
}
},false)
- js阻止事件捕获跟冒泡: e.stopPropagation
- js阻止默认事件:e.preventDefault()
垃圾回收
什么是垃圾?
不被需要的就是垃圾
如何回收垃圾?
标记回收算法
- 标记
从根节点(Root)出发,遍历所有的对象。
可以遍历到的对象,是可达的(reachable)。
没有被遍历到的对象,不可达的(unreachable) - 回收不可达的对象
- 内存整理
什么时候回收垃圾
浏览器进行垃圾回收的时候,会暂停 JavaScript 脚本,等垃圾回收完毕再继续执行。
对于普通应用这样没什么问题,但对于 JS 游戏、动画对连贯性要求比较高的应用,如果暂停时间很长就会造成页面卡顿。
什么时候进行垃圾回收很重要。
- 分代收集
按照变量的生命周期,临时对象为新时代,长久对象为老生代,临时变量用完就回收,window、dom等长久对象延后回收(如关闭浏览器标签) - 增量收集
将垃圾收集工作分成更小的块,每次处理一部分,多次处理。 - 闲时收集
在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
闭包
闭包的官方定义(MDN)
一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
为什么要说闭包?首先要知道闭包的作用
- 隐藏变量,避免污染全局变量
- 提供对局部变量的间接访问
- 避免垃圾回收(划重点)
闭包的应用场景
作者没用到过,原因是面向对象编程很少回需要用到闭包,从列出来的三点闭包的作用就可以知道,对象的封装、继承、多态本身具备。(个人看法)
weakMap weakSet
WeakSet 、weakMap里面的引用,都不计入垃圾回收机制,因此,WeakSet 、weakMap适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 、weakMap里面的引用就会自动消失。他们都是用来解决内存泄漏问题的。(作者太菜了,还没用过,在node开发上面会很有用)
知道垃圾回收原理之后要注意什么?
1、减少使用全局变量,如尽量减少window上挂载对象
2、用完的对象记得回收,避免内存泄漏(垃圾没有被及时回收就是内存泄漏),如
- addEventListener ,removeEventListener这种全局监听要成对使用
- 挂载在window上的对象要手动回收调(赋值为null)
- 在vue等框架中,beforeDestroy 要及时销毁全局变量如 o n , on, on,off、Vuex 的 $store、第三方库等
- 使用弱引用 weakMap、weakSet
- 闭包不会导致内存泄漏,因为你用了它就是不想变量被回收,除非你滥用。
3、内存泄漏会导致页面卡顿甚至崩溃,当页面奔溃时,很可能就时因为内存泄漏,而内存泄漏就要检查哪些垃圾没有回收,检查时可以看有哪些全局的变量是需要但是忘记回收的
浏览器的几个节点事件
1、load跟unload,加载跟关闭,一般在onload之后执行js,有些恶心的网站在unload的时候弹alert,阻止你关闭页面
2、onpopstate、hashchange、statechange当窗口历史记录(url)改变时触发,可以实现路由,感兴趣可以看这个
从浏览器原理看优化
- 减少http请求
图片合并
使用缓存
js、css压缩合并
减少后端接口的调用,该合并的接口就合并成一个
请求使用keep-alive - 减少静态文件体积
js压缩、css压缩
图片使用base64
代码拆分 - 减少阻塞
js放在html后面
大图片用img,想要早点看到的图片放background
能不用iframe就不用
懒加载 - 代码层面
减少回流,灵活使用position,尽量用class不用style
减少代码体积(代码冗余)
使用web worker处理长时间计算的事务
异步执行代码
全局变量及时回收,避免内存泄漏