浏览器进程模型

谈到浏览器中的事件循环就不得不谈到浏览器的进程模型了,浏览器是一个多进程多线程的应用程序。其主要原因是因为:其内部工作极其复杂,为了不相互影响导致全部崩溃,因此当启动浏览器后他会启动多个进程进行相互协作,其中必不可少的为:

  • 浏览器主进程

    主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。

  • 网络进程

    负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。

  • GPU 进程

    用于加速渲染进程中的图形和动画处理。它利用计算机的图形处理单元(GPU)来处理复杂的图形计算,如渲染3D效果、视频播放、图像处理等。GPU进程将渲染进程发送的图形命令转换为GPU可以理解的指令,并将渲染结果返回给渲染进程。

  • 多个插件进程

    主要是负责插件的运行,插件运行中可能会出现崩溃,为了防止由于插件崩溃的导致浏览器崩溃,因此使用插件线程来进行隔离,以确保插件崩溃时不会对页面和浏览器造成影响

  • 多个渲染进程

    渲染进程是负责处理网页内容渲染的进程。它负责解析HTML、CSS和JavaScript,将网页内容转换为可视化的图像,并将其显示在浏览器窗口中。渲染进程还处理用户交互、处理鼠标和键盘事件、执行JavaScript代码等。

    默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。未来可能调整为默认为每个域名为一个渲染进程。参考:chrome官方说明文档

image-20230715210745955

渲染主进程如何工作

渲染主进程的工作过程又称为事件循环(消息循环),渲染主进程的工作包括但不限于如下:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画 60 次
  • 执行全局 JS 代码
  • 执行事件处理函数
  • 执行计时器的回调函数

由于该进程执行的任务非常之多,因此如果不进行任何处理而去顺序执行任务,则会造成浏览器的卡顿,例如:用户正在执行一个函数,执行过程中,另外一个计时器到达了执行时间,此时该如何执行。因此渲染进程采用了排队机制来解决这一系列的问题。其中排队的队列称为消息队列。因此浏览器只需要每次使用该队列的第一个任务(队列遵循先进先出的原则)即可(如果存在任务)。

  1. 在最开始的时候,渲染主线程会进入一个无限循环
  2. 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务

image-20230715213407093

异步任务

浏览器为了解决排队过程中造成的不必要等待,例如:无休止的等待定时器执行完毕、网络通信后的回调函数。从而有了异步任务。

由于渲染主线程只有一个,而 JS 工作在渲染主线程中,因此 JS 在此是一门单线程的语言。

渲染主线程承担着很多工作:渲染页面,执行 JS 等,如果使用同步的方式来执行任务,则极有可能造成渲染主线程阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。

因此浏览器采用异步的的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。

在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

image-20220810104858857

消息队列

任务没有优先级,但消息队列有优先级。

参考:event-loops

根据 W3C 的解释,消息队列(任务队列)指的的集合,而非队列。事件循环应该有一个或者多个任务队列,但微任务则不属于消息队列。

其实也就是说除了一个任务队和一个微任务队列

20230716174016000224

而消息队列根据任务类型可以划分多个队列,由浏览器根据自身实际情况自行判断优先级(先执行哪个队列的内容)。

在目前 chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
  • 微队列:用户存放需要最快执行的任务,优先级「最高」

事件循环

从代码中实践事件循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
new Promise((resolve)=>{
console.log('promise4')
resolve()
}).then(()=>{
console.log('promise5')
})
}

async function async2() {
console.log("async2");
}

console.log("script start");

setTimeout(function () {
console.log("setTimeout1");
new Promise(resolve=>{
console.log('promise6')
resolve()
}).then(()=>{
console.log('promise3')
})
}, 0);

setTimeout(function () {
console.log("setTimeout2");
}, 0);

async1();

new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});

console.log("script end");

await可以理解为Promise的一个语法糖,例如如上await async2();可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
new Promise( resolve =>{
async2()
resolve()
}).then( () => {
console.log("async1 end");
new Promise((resolve)=>{
console.log('promise4')
resolve()
}).then(()=>{
console.log('promise5')
})
})

因此对于上述代码的执行顺序如下:

  1. 首先加载所有代码,并依次执行所有同步代码,异步代码则丢入对应的队列由对应的线程进程处理。

    主线程:console.log("script start");;将两个setTimeout丢入到延迟队列;执行async1(),进入该函数,执行同步代码console.log("async1 start");console.log("async2"),遇到微任务(await后的即为微任务),将其丢入微任务队列,此时将暂时退出该函数,执行 35 行的 Promise 中的同步部分即console.log("promise1");resolve();;最后执行console.log("script end"),此时主线程的代码执行完毕,将在队列中进行取出任务并执行。

    微任务队列:[async1函数产生的 Promise , 35行处产生的 Promise, ]

    延迟队列:[setTimeout1, setTimeout2]

  2. 按照队列优先级,从最高级优先的队列中执行第一个排队的任务:async1函数产生的 Promise,输出console.log("async1 end")此时遇到 Promise 执行其同步部分console.log('promise4');resolve(),并将异步内容再次推入到队列

    微任务队列:[35行处产生的 Promise, async1中 Promise 产生的任务]

    延迟队列:[setTimeout1, setTimeout2]

  3. 此时渲染主线程再次从队列中读取任务,并执行35行处产生的 Promise中的console.log("promise2")

    微任务队列:[async1中 Promise 产生的任务]

    延迟队列:[setTimeout1, setTimeout2]

  4. 继续执行微任务队列中的任务:console.log('promise5')

  5. 此时微任务队列为空,则执行下一个队列中的任务console.log("setTimeout1");console.log('promise6')并产生微任务,将其推入到微任务队列

    微任务队列:[setTimeout1产生的微任务]

    延迟队列:[setTimeout2]

  6. 此时微任务队列又有了任务,因此将再次执行微任务队列中的任务console.log('promise3')

  7. 此时只剩下延迟队列中的console.log("setTimeout2")函数执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
script start
async1 start
async2
promise1
script end
async1 end
promise4
promise2
promise5
setTimeout1
promise6
promise3
setTimeout2

参考

此文虽然有参考,但还是有些内容无法从参考文中得到明确的答案,因此部分结论为我个人根据资料推测得出。

如有错误,还望大佬指正。