随着浏览器的复杂度急剧提升,w3c不再使用宏队列的说法。
浏览器的进程模型
谈事件循环之前,先来看看进程和线程
什么是进程
1. 每个程序运行都需要有自己的一个块内存,这块内存就可以理解为进程。2. 每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。
什么是线程
1. 一个进程可以有多个线程,但是至少得有一个线程。2. 进程开启后会自动创建一个线程来运行代码,该线程称为主线程。
3. 主线程忙不过来了,主线程就会启动更多的线程来处理。
通俗的理解
-
多个家庭作为多个进程:
- 想象一个社区有许多家庭,每个家庭是一个独立的进程。每个家庭管理自己的事务,不直接共享物资或资源。
- 各个家庭在相互独立的同时,也可能通过社区活动、电话或社交媒体进行沟通(类似于进程间通信IPC)。
-
家庭中的主线程 :
- 每个家庭有一个主要的协调者,比如妈妈,当做主线程。她负责管理家庭的日常活动和资源分配。
- 妈妈作为主线程,负责启动和协调家里的任务,比如安排谁去做什么事情,确保家庭正常运作。
-
家庭中的其他线程:
- 家庭中的其他成员,比如孩子和爸爸,作为其他线程执行具体的任务。比如孩子去写作业,爸爸做饭。
- 这些线程在执行各自的任务时,受到主线程(妈妈)的协调。例如,妈妈在打扫卫生时听到有人敲门,她会要求孩子去开门,这说明主线程可以调用和协调其他线程来完成任务。
-
主线程的重要性和多线程的协作:
- 主线程(妈妈)在面临家庭事务时,可以管理和指导其他线程(孩子或爸爸)去执行任务。
- 如果家庭中有突发事件,比如有人生病或需要紧急的帮助,主线程会迅速做出反应并协调其他成员行动。
-
多个家庭(进程)如何协作:
- 各个家庭(进程)之间可以通过社区会议、电话或社交媒体等方式进行信息交流和协作(类似于进程间通信IPC)。
- 即使一个家庭(进程)因为某些原因(如主线程生病)而无法正常运作,其他家庭(进程)仍然可以正常活动。
浏览器有哪些进程和线程
可以在浏览器任务管理中查看当前的所有进程
其中,最主要的进程有
浏览器进程
主要负责页面的显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。网络进程
负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。渲染进程(这次的重点讲解)
渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML、CSS、JS代码。默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页面之间不相互影响(后面可能会改变这种模式,如果浏览器窗口开的特别多,就非常消耗内存)
渲染主线程的工作流程
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于- 解析HTML
- 解析CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面话60次
- 执行全局JS代码
- 执行事件处理函数
- 执行计时器的回调函数
比如:
- 渲染主线程每执行完一个任务后,就会从任务队列(其实叫消息队列,方便理解所以叫任务队列)取一个任务进行执行
- 网络、事件触发、定时器等线程监听到有任务已经完成后,就会把当前任务加入到任务队列
- 同时当前在执行的任务中如果还会产生新任务,如果是定时器任务,就会把任务丢给定时器线程,定时器线程监听计时结束后,就会把这个任务添加到任务队列,等待渲染主线程执行。其他线程的任务也是同理。
当然了,浏览器的事件循环肯定没有这么简单,但是这是最核心的流程,只有理解上面这个流程。才能继而理解下面的讲述的流程。
浏览器的事件循环
随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法在目前的chrome的实现中,至少包含下面的队列:
- 延时队列:用于存放计时器到达后的回调任务
- 交互队列:用于存放用户操作后产生的事件处理任务
- 微队列:用户存放需要最快执行的任务。(添加微队列的主要方式有:Promise、MutationObserver)
问题和重点来了,既然有这么多个队列,那当每个队列都有一个任务要执行时,先执行哪一个呢
- 任务没有优先级,在队列中先进先出,但是队列是有优先级的。
- 由高到低:微队列 > 交互队列 > 延时队列。
根据 W3C的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
- 浏览器必须准备好一个微队列,微队列中的任务优先所有的其他任务执行
- 如果都是定时器类型的任务,就必须都在一个队列,不一定是延时队列,也可以是交互队列。但是不能一部分在延时队列,一部分在交互队列。然后队列之间的优先级由浏览器厂商自行实现。
- 浏览器必须准备一个微队列,执行其他队列的任务之前,必须先把当前微队列中的任务执行完,才能执行其他队列的任务。
来几个题目巩固一下理解
第一题:setTimeout(()=>{
console.log(1)
},0)
console.log(0)
答案:0 1
执行顺序:
- 先执行全局(全局的权重最高,像这里console.log(0)就是全局代码,没有什么微队列、定时器、网络代码包裹)
- 当代码执行到setTimeout时,会把里面的回调函数(这个就是回调函数:()=>{
console.log(1)
})放入计时器线程进行监听
- 然后执行console.log(0) 打印0
- 同时呢计时器线程监听到计时结束,就把回调函数放入到延时队列等待渲染主线程进行执行。
- 当渲染主线程执行了全局代码后,就会去队列中去取任务,取到回调函数,然后进行执行,打印1。
第二题:
setTimeout(()=>{
console.log(1)
},0)
//把then里面的函数放入微队列
Promise.resolve().then(()=>{
console.log(2)
})
console.log(3)
答案:3 2 1
执行顺序:
- 先执行全局
- 当代码执行到setTimeout时,会把里面的回调函数(这个就是回调函数:()=>{
console.log(1)
})放入计时器线程进行监听
3.当代码执行到Promise时,发现是微任务,就会把回调函数放入到微队列 - 然后执行console.log(3) 打印3
- 同时呢计时器线程监听到计时结束,就把回调函数放入到延时队列等待渲染主线程进行执行。
- 当渲染主线程执行了全局代码后,会先去微队列,把微队列的任务执行完,打印2
- 微任务的全部执行完后,查看其他队列,有无任务,发现延时队列还有任务,于是便执行延时队列的任务,打印 1
第三题:
function a(){
console.log(1);
Promise.resolve().then(function(){
console.log(2)
})
}
setTimeout(function(){
console.log(3)
Promise.resolve().then(a)
},0)
Promise.resolve().then(function(){
console.log(4)
})
console.log(5)
答案:5 4 3 1 2
执行顺序:
1.全局执行,a 是个函数,没有调用,不管。
2.继续执行,执行到setTimeout,把setTimeout里面的回调函数放入到计时器线程进行监听。
3.继续执行,到全局的Promise里面的回调函数(function(){ console.log(4) }),放入到微队列。
4.执行console.log(5),打印5。到此全局已经执行完毕,开始执行队列里面的任务
5.计时器线程监听到,setTimeout的倒计时已经结束。把回调函数放入到延时队列。
6.渲染主线程开始从队列里面去拿任务。但是还不能拿第五步延时队列的的任务,因为微队列里面还有任务没有执行完。所以先执行function(){ console.log(4) },打印4。然后再执行function(){
console.log(3)
Promise.resolve().then(a)
}。打印3。这时又有一个微任务a,加入到微队列,等待下一轮事件循环。
7.上一轮事件循环执行完后,开启新一轮的事件循环。渲染主线程从队列中拿任务,先执行一下微队列a函数,再执行a函数的过程中,打印了1,然后又添加了一个微任务到微队列,这个微任务执行完毕,继续执行其他队列的任务。这里发现其他队列没有任务可以执行,当前事件循环结束。
8.开启新一轮事件循环,先执行微队列中的任务console.log(2),微队列清空后,查看其他队列中已经没有任务了,执行完毕。