最近又复习了一下基础知识,做个记录。
一. 浏览器的进程和线程
浏览器是一个多进程,多线程的应用程序。
什么是程序,什么是线程,什么进程?
举个例子:
程序是一个工厂,而进程就是工厂中的一条流水线,工厂可能有一条流水线,也可能有多条流水线。
进程就是流水线上的工人,可能只有一个工人,也可能有多个工人。
当程序启动的时候,相当于工厂开始进行生产,流水线(进程)也启动,流水线上的工人(线程),都有自己的本职工作,轮到自己的环节,就拿过来处理。
1. 什么是进程
程序的执行就是进程。
也可以把进程看成一个独立的程序,在内存中有其对应的代码空间和数据空间,一个进程所拥有的数据和代码只属于自己。
进程是资源分配的基本单位,也是调度运行的基本单位。
2. 什么是线程
线程是进程中单独运行的一路程序。换句话说,就是一个进程可以包含多个线程,并且至少有一个主线程,同时同一进程的线程共享该进程的代码和数据。于此同时,每一个线程又都有自己的堆栈,这些堆栈对于线程来说是私有的。线程是处理机调度的基本单位。
那为什么要引入线程呢?
a) 便于调度
b) 线程可以共享进程的数据和代码,从而比进程需要通过消息才能通信来得更加简单。启动和切换的速度也比进程快。
c) 具有高并发性,可以启动多个线程执行同程序的不同部分。
d) 充分利用处理器的功能。让每一个处理器上运行不同线程,从而实现应用程序的并发性。
3. 进程和线程的区别
a) 一个进程可以拥有多个线程,而一个线程同时只能被一个进程所拥有。
b) 进程是资源分配的基本单位,线程是处理机调度的基本单位,所有的线程共享其所属进程的所有资源与代码。
d) 线程执行过程之中很容易进行协作同步,而进程需要通过消息通信进行同步。
d) 线程的划分尺度更小,并发性更高。
e) 线程共享进程的数据的同时,有自己私有的的堆栈。
f) 线程不能单独执行,但是每一个线程都有程序的入口、执行序列以及程序出口。它必须组成进程才能被执行。
浏览器启动的时候,会开辟多个进程,前端相关的主要有:
浏览器进程
网络进程
渲染进程
每一个进程,都有一块自己独立的内存空间。
多个进程,多个独立不相干的内存空间有什么好处呢,起一个隔离作用,一个进程崩溃了,不会影响其他的进程,比如,网络进程崩溃了,不会影响网页渲染。
每打开一个页面,就会创建一个渲染进程,这样的好处是,每个页面互不相干,一个页面崩溃不会影响别的页面,谷歌浏览器现在就是这样做的,但是,同样的,每一个页面,开一个渲染进程,很消耗内存,据说,谷歌浏览器已经尝试用新的做法来减少内存开销。
再来说一说几个进程的作用。
浏览器进程:这个就是负责浏览器界面(不包含网页的界面,这里的界面指的是比如浏览器上的前进、后退按钮,浏览器菜单这些)、监听用户操作(这个‘用户操作’指的是你点击了什么按钮,包括点击网页内部的按钮,键盘等用户所有操作的事件)
网络进程:这个就是负责网络通信,前端Ajax请求,就是靠它完成
渲染进程:这个是前端关系最紧密的进程,渲染进程启动后,它会开启一个渲染主线程,注意,这个是渲染主线程,线程是进程下面的工人。
前端代码都是再这个渲染主线程里执行的。
常常看到一句话:js是一门单线程语言。现在明白是哪个单线程了吧,就是渲染主线程这个单线程。
二、渲染主线程
弄清了浏览器进程、线程,再说一说这个渲染主线程。
渲染主线程的事情很多,HTML、css、js,都归它管。
看看它做了什么:
1解析HMTL,一开始的时候,页面中的HTML就是一段html字符串,需要根据规则解析它,生成dom树,dom树是一个对象,用console.dir(document)可以将它打印出来
2解析CSS,这一步是将样式收集起来,创建CSSOM树,也是一个对象,它的样子可以通过document.styleSheets查看,这是一个类数组,里面有很多样式表规则,我们还可以通过它操作样式,document.styleSheets[0].insertRule("div { color:green;}")
3计算各个元素的样式,比如把em、%的数值,换算成像素单位,计算元素所有的几何信息
4页面布局,display:none的元素没有几何信息,不会生成到布局树里面。
5处理图层,分层的目的是为了提高效率,如果某一层发生了变化,只处理当前层级就可以了,不用影响其他的元素层级,浏览器默认的滚动条,就是一个单独的层级,因为滚动条是经常变化位置的。这些层级也可以再浏览器里面看的到。
7渲染出画面
8执行全局的JS代码
9执行事件、延时器等各个地方的回调函数
主渲染线程解析Html字符串的时候,是从头到尾解析的,一开始是doctype、html...一直解析到最后一个字符,这一步就生成了dom树,cssom树。
因为js代码只要执行一遍就可以的,所以没有js树这种东西,遇到就执行。
第二步,就是将dom树对象、cssom树对象一起计算元素的样式信息
第三步,布局,各个元素对应浏览器中的位置
第四步,元素会有层级关系,要计算出来,z-index会影响这个层级计算的因素,但不是唯一的,浏览器有自己的逻辑
第五步,最后HTML字符串变成了一系列的像素点和对应的颜色,通过渲染,把dom元素画出来,。
这五步是我自己假设的大致步骤,只是为了方便描述流程,浏览器真实渲染过程肯定要复杂的多。
A. 预解析线程
如果主渲染线程解析的时候遇到css代码的时候,浏览器会启动一个叫做预解析的线程。
渲染主线程就会把css解析的工作交给预解析线程,然后渲染主线程就不管刚才交出的的css代码了,一直到预解析线程把解析结果返回给渲染主线程,渲染主线程将结果生成CSSOM树。
因此呢,css是不会阻塞页面加载的,因为css解析的主要工作就不在渲染主线程这里,所以碰到link样式,需要下载远程的css文件时,预解析线程你自己通过网络通信进程去下载就好了,下载成功也好,失败也罢,对主渲染线程来讲,都是没有感觉的,如果css文件下载失败,无非就是返回的结果不符合预期而已。
假设有一个叫1.css文件下载很麻烦,花了很长时间,会有什么影响呢?
对渲染主线程的渲染流程不会有任何影响,所以用户看到的可能是缺少1.css这部分样式的页面,然后过了好几秒,1.css才姗姗来迟,这时候会怎么样?
渲染流程会重新计算元素样式,把第二步到第五步,重新走一遍。然后用户,看到就是,页面闪了一下,页面样式变好了。
B. 回流、重绘
其实两个东西已经说过了,比如,有一个div颜色被js操作,发生了改变,布局没发生变化,这时候,就会重新绘制页面,也就是把第五步,在执行一遍,这个过程叫重绘。
如果你页面有元素发生了几何变化,比如大小变了,位置变了,那就需要重新计算、重新布局、重新渲染,这个过程叫回流,相当于从第二步开始,再走一遍。
所以我们做dom操作的时候,一定要尽量避免回流的发生。
C. transform
transform不会影响元素的样式属性,所有改变,理论上来讲,都是临时的,它发生在GPU,都不在渲染主线程里发生,所以效率也高。
如果非要对元素做left,top改变动画,可以考虑将元素设置成position:absoute;脱离文档流,这样它就是一个单独的层级,不会影响其他元素。
D. 事件循环、任务队列、异步
js是运行在渲染主线程里面的,所以,js是单线程语言。
如果在解析HTML字符串的时候,js卡住了,渲染主线程就会被阻塞,卡在解析HTML字符串这一步,后面的步骤就走不下去了。
基于js单线程这个特点,理论上,js的代码都应该是同步执行的,但是网页中常常有一些无法避免的耗时操作,比如ajax请求,如果碰到这种情况,难道还要页面停止运行,闲在那里一直等待ajax返回吗?
当然不是,所以诞生了事件循环,诞生了异步。
那么,事件循环能完全解决上面js卡住,影响HTML解析的问题吗?
答案是不行,所以你必须尽量保证你代码的质量,不要运行到一半突然来个报错,或者不要直接把某段消耗大量时间的代码放到主流程里面,尽量选择一些规避手段。
事件循环,它是渲染主线程的一种工作方式。
假设有一段js代码,里面有ajax、settimeout,当然也有普通的代码,碰到这种情况,渲染主线程会怎么做呢?
渲染主线程什么也不会做,它只会执行那些普通的代码,至于ajax、settimeout它们的回调函数,会被存到一个队列里面,让这些回调函数去排队,等普通代码全部执行完成,渲染主线程空闲的时候,它就会循环队列,将里面的回调函数取出执行,什么时候轮到回调函数,什么时候取出执行。
所以又引出了任务队列的概念。
与之相关的还有宏任务、微任务的概念,不过官方好像没有宏任务这个词,微任务也是es6里面提出的,es6里面的promise、await的回调就属于微任务。
既然官方没有,我们就不提宏任务,而是另外三个东西:微任务队列、交互任务队列、延时任务队列。
这三个队列就是消息队列里面的东西,还有其他队列,不做考虑,它们的排队权限有高低之分,微任务队列是权限最高的,交互任务次之,延时任务最低。
微任务:promise、await
交互任务: 用户操作事件
延时任务: settimeout、setinterval
<script type="text/javascript">
setTimeout(() => {
console.log('settimeout 1 .....')
new Promise((resolve, reject) => {
console.log('promise 1 .....')
resolve()
}).then(() => {
console.log('promise then 1 .....')
new Promise((resolve, reject) => {
console.log('promise 2 .....')
resolve()
}).then(() => {
console.log('promise then 2 .....')
})
})
}, 0)
setTimeout(() => {
console.log('settimeout 2 .....')
}, 0)
new Promise((resolve, reject) => {
console.log('promise 3 .....')
resolve()
}).then(() => {
console.log('promise then 3 .....')
})
console.log('最外部的打印.....')
</script>
```js
打印结果:
promise 3 .....
最外部的打印.....
promise then 3 .....
settimeout 1 .....
promise 1 .....
promise then 1 .....
promise 2 .....
promise then 2 .....
settimeout 2 .....
事件循环、任务队列,搞清楚这两个概念,就明白什么是异步了。
因为js是单线程的,渲染主线程为了不被js阻塞,所以浏览器使用了一种事件循环的工作方式,这个概念就叫异步。
再说一句:单线程是异步产生的原因,事件循环是异步的实现方式。
网友评论