浏览器架构和事件循环

Chrome浏览器架构

Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
image-20210930012419754

Browser Process

  • 子进程的管理(网络、渲染、GPU)
  • 负责包括地址栏,书签栏,前进后退按钮等部分的工作;
  • 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;

Network Service

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

Renderer Process

  • 负责一个 tab 内关于网页呈现的所有事情,进程启动后,会开启一个渲染主线程,负责执行HTML、CSS、JavaScript代码

Plugin Process

  • 负责控制一个网页用到的所有插件,如 flash

GPU Process

  • 负责处理 GPU 相关的任务

由于一个tab标签页都有一个独立的渲染进程,所以一个tab异常崩溃后,其他tab不会受到影响。

一个渲染进程包括

  • 主线程
  • HTTP请求线程
  • 定时触发线程
  • 事件触发线程
  • GUI线程

渲染进程中的主线程是如何工作的

包括但不限于

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

浏览器JS异步执行原理

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
  4. 主线程不断重复上面的第三步。

浏览器中的事件循环

事件循环是浏览器渲染主线程的工作方式,主线程会将不同的任务交给不同线程处理,其他线程处理完后会把回调函数放到对应的队列里

image-20210930190158487

Event Loop(事件循环)中,每一次循环称为 tick, 每一次 tick 的任务如下:

  1. 调用栈执行同步代码,遇到异步任务交给各自的线程处理(其他线程处理完后,会把异步任务的回调加到对应的任务队列中)
  2. 调用栈清空后,检查 Microtask 队列是否存在 Microtask,如果存在则不停的执行,直至清空 Microtask 队列
  3. 更新render,dom渲染(每一次事件循环,浏览器都可能会去更新渲染)
  4. Task 队列中取出一个 Task 执行
  5. 重复以上步骤

任务队列(Task Queue)

延时任务队列(Task)
  • 定时器APIsetTimeoutsetInterval 相应的回调函数
交互任务队列
  • 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 script 元素中运行代码)。

  • 触发了一个事件,将其回调函数添加到任务队列时,比如交互相关(用户点击、视口变化、键盘事件)

  • 优先级:用户交互相关任务 > 定时任务 > 网络 / 通信任务。

  1. script标签
  2. UI交互(click、mousedown、keydown、touchstart 等)
  3. 网络I/O(XHR/Fetch 完成回调)
  4. setTimeout、setInterval
  5. requestAnimationFrame
  6. MessageChannel 的 onmessage 回调

微任务队列(MicroTask Queue)

  1. MutationObserver
  2. Promise.then catch finally
  3. queueMicrotask()

对于以下demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise((r) => {
r();
})
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3))

new Promise((r) => {
r();
})
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))

// 输出
// 1 4 2 5 3 6

Promise.prototype.then() 会隐式返回一个新 Promise

如果 Promise 的状态是 pending,那么 then 会在该 Promise 上注册一个回调,当其状态发生变化时,对应的回调将作为一个微任务被推入微任务队列

如果 Promise 的状态已经是 fulfilled 或 rejected,那么 then() 会立即创建一个微任务,将传入的对应的回调推入微任务队列

对于以下demo:

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
console.log('start')
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('promise in timeout');
resolve();
});

console.log('promise after timeout');
}).then(() => {
console.log('promise4');
Promise.resolve().then(() => {
console.log('7777')
})
}).then(() => {
console.log('promise5');
});

setTimeout(() => {
console.log('99999');
}, 0);
Promise.resolve().then(() => {
console.log('promise3');
});

console.log('end');
// 输出:
// start
// promise after timeout
// end
// promise3
// promise in timeout
// promise4
// 7777
// promise5
// 99999
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log(1);
async function async1() {
await async2();
console.log(2);
await async3();
console.log(3);
}

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

async function async3() {
console.log(5);
}

async1();
console.log(6);
// 1 4 6 2 5 3

Node中的事件循环

Node中的任务队列和异步API

  • Timer队列:定时器相关API,setTimeoutsetInterval
  • Poll队列:IO操作,文件读写、数据库操作、网络请求
  • Check队列setImmediate(node环境中独有)
  • NextTick队列process.nextTick(node环境中独有)

Node中事件循环执行流程

image-20210930190158487

  1. 调用栈执行同步代码,遇到异步任务交给各自的线程处理(其他线程处理完后,会把异步任务的回调加到对应的任务队列中)
  2. 调用栈清空后,执行nextTick队列process.nextTick回调,直至清空nextTick队列
  3. 清空微任务队列Promise.then里的回调
  4. Timer队列里取出setTimeoutsetInterval回调执行,若nextTick队列微任务队列不为空,重复步骤2/3
  5. Poll队列里取出文件IO,网络IO相关操作的回调执行,若nextTick队列微任务队列不为空,重复步骤2/3
  6. Check队列里取出setImmediate回调执行,若nextTick队列微任务队列不为空,重复步骤2/3
  7. Poll队列等待,判断是否有新的异步任务,若Poll队列有任务,直接执行,若其他队列有任务,则继续下一个 Tick

Timer-> Poll -> Check称之为一个Tick
nextTick队列,总会在下个Tick运行之前执行

遇到Poll队列时,会暂停等待另外两个队列,此时如果有新的IO操作,会直接执行(因为Node的设计就是优先处理IO操作

对于以下demo:

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
43
44
45
46
47
48
console.log("sync");
process.nextTick(() => {
console.log("next tick 1");
});

setImmediate(() => {
console.log("setImmediate 1");
});

Promise.resolve().then(() => {
console.log("promise 1");
});

process.nextTick(() => {
console.log("next tick 2");
});

setTimeout(() => {
console.log("setTimeout 1");
}, 0);

setImmediate(() => {
console.log("setImmediate 2");
});

setTimeout(() => {
console.log("setTimeout 2");
}, 0);

Promise.resolve().then(() => {
console.log("promise 2");
});

Promise.resolve().then(() => {
console.log("promise 3");
});

// 输出:
// sync
// next tick 1
// next tick 2
// promise 1
// promise 2
// promise 3
// setTimeout 1
// setTimeout 2
// setImmediate 1
// setImmediate 2

setTimeout和setImmediate的问题

1
2
3
4
5
6
7
8
9
setTimeout(() => {
console.log('定时器')
}, 0)

setImmediate(() => {
console.log('setImmediate')
})

// 输出可能是先“定时器”,也可能是先“setImmediate”

setTimeout在node中延迟时间最小取值是1ms,而不是0ms(最快1ms调用),所以这里谁先输出是不一定的,若要让“setImmediate”先执行,可以包在文件IO的回调里

1
2
3
4
5
6
7
8
9
fs.readFile(_filename, () => {
setTimeout(() => {
console.log('定时器')
}, 0)

setImmediate(() => {
console.log('setImmediate')
})
})