浏览器架构和事件循环

chrome浏览器架构

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

  • Browser Process:
  1. 负责包括地址栏,书签栏,前进后退按钮等部分的工作;

  2. 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;

  • Renderer Process:
  1. 负责一个 tab 内关于网页呈现的所有事情
  • Plugin Process:
  1. 负责控制一个网页用到的所有插件,如 flash
  • GPU Process
  1. 负责处理 GPU 相关的任务

image-20210930012419754

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

一个渲染进程包括

  • JS引擎线程
  • HTTP请求线程
  • 定时触发线程
  • 事件触发线程
  • GUI线程

浏览器JS异步执行原理

执行JS代码的线程只有一个,是浏览器提供的JS引擎线程,浏览器中还有

image-20210930012033902

浏览器中的事件循环

image-20210930190158487

js引擎解析代码时,遇到同步任务->放入执行栈直接执行,遇到异步任务(比如ajax),交给网络线程处理,完成后将对应回调放入异步任务队列,当执行栈清空时,循环检测异步任务队列,将异步队列中的回调放入执行栈中执行。

宏任务和微任务

Dom渲染会在微任务结束后,宏任务执行前

宏任务
# 浏览器 Node
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame
微任务
# 浏览器 Node
process.nextTick
MutationObserver
Promise.then catch finally

事件循环加入宏任务微任务

  1. Call Stack调用栈清空
  2. 清空当前微任务队列,若有新增微任务,会加入当前队列尾部继续执行。
  3. 尝试DOM渲染
  4. 执行event loop
  5. 取出任务队列里的一个宏任务,入栈执行
  6. 返回1

对于以下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

Node中的事件循环

  1. 执行同步代码
  2. 执行nextTick队列里process.nextTick回调
  3. 执行微任务队列Promise.then里的回调
  4. 执行Timer队列里setTimeout,setInterval回调
  5. 执行Poll队列里文件IO,网络IO相关操作
  6. 执行Check队列里setImmediate回调
  7. Poll队列等待,判断是否有新的异步任务,若Poll队列有任务,直接执行,若其他队列有任务,则继续下一个tick

Node中异步API分类

  • 定时器API:setTimeout、setInterval(Timer队列)
  • IO操作:文件读写、数据库操作、网络请求(Poll队列)
  • node独有:process.nextTick、setImmediate(Check队列)

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

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')
})
})

process.nextTick(nextTick队列)

nextTick队列,总会在下个Tick运行之前执行

微任务队列

nextTick->微任务->Timer->Poll->Check