浏览器工作原理——消息队列与事件循环

页面事件循环系统

了解页面到底是如何运行的
了解浏览器的主线程是如何动作的

线程模型(一)—— 主线程依照代码顺序,依次执行任务

用单线程是按顺序,依次处理确定好的任务

1
2
3
4
5
var a = 1 + 2; // task1
var b = 2 + 3; // task2
var c = 3 + 4; // task3

console.log(a, b, c) // task4

线程模型(二)—— 事件循环机制处理线程执行过程中接收的新任务

线程运行过程中加入新的任务
引入 循环语句事件系统,在线程执行过程中接收并处理新任务。

采用事件循环机制,在线程运行过程中接收并执行新任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// main thead
void MainThread() {
// for 循环机制
for(;;) {
int first_num = GetInput()
int second_num = GetInput()
result_num = first_num + second_num
print('result:' + result_num)
}
}
// GetInput 获取用户输入,并返回
int GetInput() {
int input_number = 0;
cout<<"请输入一个数:";
cin>>input_number;
return input_number
}
  1. 主线程中 引入循环机制,即在线程语句加入 for 循环,线程会一直循环执行;
  2. 引入事件,如在线程运行过程中,等待用户输入,线程暂停,接收到用户输入后,线程激活,继续执行。

线程模型(三)—— 消息队列管理 IO 线程传递的新任务,循环机制从消息队列中取任务执行

引入 消息队列 接收其它线程发送过来的任务。
使用消息队列管理IO线程传递的任务队列先进行出,任务添加在队列尾部,从头部取出

消息队列是一种数据结构,可以存放要执行的任务。特点先进先出,添加到尾部,从头部取出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 构造队列
class TaskQueue {
pulic:
Task takeTask() // 取出队列头部的一个任务
void pushTask(Task task); // 添加一个任务队列的尾部
}

// 主线程从队列中读取任务
TaskQueue task_queue
void ProcessTask();
void MainThred() {
for(;;) {
Task task = task_queue.takeTask()
processTask(task)
}
}

其它线程想要发送任务让主线程执行,只需要将任务添加到消息队列中就可以了。由于多个线程操作同一个消息队列,所以在添加任务和取出任务时还会加一个同步锁

线程模型(四)—— 处理其它进程发送过来的任务

如果其它线程想要发送任务给页面主线程,那么需要先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。

渲染进程专门有一个IO线程用来接收其它线程传进来的消息,IO线程会将这些消息组装成任务发送给渲染主线程

消息队列中的任务类型

事件类型

内部消息类型
输入事件(鼠标事件)
微任务
文件读写
WebSocket
javaScript定时器
页面相关的事件
javaScript 执行
解析 DOM
样式计算
布局计算
css 动画

以上事件是在主线程中执行的,所以在编写 web 应用程序时,需要衡量这些事件所占用的时长、并解决单个任务占用主线程过久的问题。

主线程如何安全退出

chrome 确定退出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否设置退出标志。如果有,直接中断所有任务,退出线程。

1
2
3
4
5
6
7
8
9
10
TaskQueue task_queue
void ProcessTask()
bool keep_running = true
void MainThread() {
for(;;) {
Task task = task_queue.taskTask()
ProcessTask(task)
if(!keep_running) break; // 设置退出标志后,直接退出线程循环
}
}

页面中使用单线程的缺点——微任务解决效率和实时性问题

如何处理高优先级的任务

场景:在处理监听 DOM 节点变化(即 DOM 插入、修改、删除等),根据这些变化处理相应的业务逻辑的场景。

处理方式:通常是采用观察者模式——利用 javaScript
设计监听接口,当变化发生时,渲染引擎同步调用这些接口。

问题:DOM 变化非常频繁,每次变化都调用直接调用相应 javaScript 接口,那么当前任务执行时间拉长,导致执行效率下降。如果将 DOM
变化做成异步消息事件,添加到消息队列尾部,又会影响到监控的实时性

解决方式:微任务用来权衡实时性与效率,微任务如何权衡效率与实时性的
消息队列中的任务称为宏任务,每个宏任务中都包含一个微任务队列。

在执行宏任务中,DOM 有变化,那么就会将变化添加到微任务列表中,这样就不会影响到宏任务的执行。解决了执行效率问题。
当前宏任务执行完成,执行当前宏任务的微任务(DOM 变化的事件都保存在微任务列表中),从而解决了实时性的问题。

如何解决单个任务执行过久的问题

javaScript 通过回调功能解决单个任务执行过久,造成后面任务等待时间太长,给用户卡顿的感觉。JavaScript 可以通过回调功能来规避,也就是让要执行 JavaScript
任务滞后执行。

浏览器如何运行的

开发者工具 –> performance 标签 –> 右上角 start profiling and load page
img
我们点击展开了 Main 这个项目,其记录了主线程执行过程中的所有任务。图中灰色的就是一个个任务,每个任务下面还有子任务,其中的 Parse HTML 任务,是把 HTML 解析为 DOM 的任务。值得注意的是,在执行 Parse HTML 的时候,如果遇到 JavaScript 脚本,那么会暂停当前的 HTML 解析而去执行 JavaScript 脚本。

总结

graph TD
module1[主线程按顺序依次执行任务]--> module2[主线程引入循环语句与事件系统,处理执行过程中接收的新任务]
module2 --> module3[主线程与消息队列解决其它线程发送的任务]
module3 --> module4[其它线程通过IPC把任务发送给渲染进程的IO线程,IO线程把任务发送给主线程]
module4 --> module5[引入微任务解决效率与实时性问题]