事件循环应该是浏览器的机制,但浏览器会执行JS,所以它会体现在JS上

浏览器的进程模型

进程

进程是应用程序执行的实例,程序运行时需要诸如一片内存空间等的资源

每个运行的应用程序至少具有一个进程,进程之间相互独立,只有在双方同意的情况下才能进行通信

Process (computing) - Wikipedia

线程

通常来说,线程是被进程独立管理的一个组件,被同一个进程管理的线程可以共享同一片内存

一个进程至少拥有一个线程,所以在进程开启后,其会自动创建一个线程运行代码,这个线程就是主线程,主线程结束意味着整个进程结束

一个进程可以拥有多个线程,当应用程序需要(宏观上)同时执行多块代码时,就会开启更多的线程

所以线程是打工人(误)

Thread (computing) - Wikipedia

浏览器的进程和线程

浏览器是一个多进程多线程的应用程序,其内部工作很复杂

为了避免相互影响、减少连环崩溃的几率,当开启浏览器时,它会自动启动多个进程

  • 浏览器进程:负责界面(不是页面哦)显示、用户交互、子进程(其他进程)管理等,会启动多个线程处理不同的任务,只有一个

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

  • 渲染进程

    • 会启动一个渲染主线程负责执行HTML、CSS、JS三剑客代码

    • 浏览器默认会为每个标签页开启一个新的渲染进程,以确保不同标签页间不相互影响

      作为优化,将来可能会变为按站点分配进程

  • GPU渲染进程:主要负责3D渲染,不一定会用到

  • 插件进程:主要负责加载第三方插件,不一定会用到

我这个不爱清理标签页的懒狗表示,这么多任务真是吓人XD

渲染主线程

渲染主线程是浏览器中最繁忙的线程,它处理的任务包括但不限于:

  • 解析HTML
  • 解析CSS
  • 计算样式(单位换算、层叠计算等)
  • 布局(元素的几何信息)
  • 处理图层(z-index)
  • 每秒绘制页面60次
  • 执行全局JS代码
  • 执行事件处理函数
  • 执行计时器的回调函数
  • 等等等等

渲染主线程可能面临各种各样的场景,例如

  • 场景A:线程正在执行一个JS函数,突然用户点击了按钮
  • 场景B:还是在执行一个函数,突然某个计时器时间到
  • 场景C:浏览器进程通知“用户点击了按钮”,同时某个计时器时间到

任务太多了,渲染主线程该如何调度任务?它的答案是:任务队列

淦哦,我懒得再自己画了

  • 最开始,渲染主线程会进入一个无限循环

  • 每次循环都会检查消息队列中是否有任务存在

    • 如果有,就取出第一个任务执行,执行完一个任务后进行下一次循环
    • 如果没有,进入休眠状态
  • 其他所有线程(包括其他进程的线程)可以随时向消息队列添加新任务

    比如浏览器进程处理的用户交互,它自身不执行JS代码,但会把JS事件回调包装成一个任务推入任务队列

    • 这个新任务会被添加到消息队列的末尾,等待执行
    • 如果此时渲染主线程是休眠状态,则将其唤醒,以此继续循环、执行任务

这个过程就被称为事件循环(Event Loop)

概念

异步

代码执行过程中,会遇到一些无法立即处理的任务,比如

  • 一段时间后才执行的任务,例如setTimeout、setInterval
  • 网络通信完成后才执行的任务,例如XHR、fetch
  • 用户操作后需要执行的任务,例如addEventListener

如果让渲染主线程只是干巴巴地傻等着这些任务的执行时机,就会导致主线程长期处于阻塞状态,让浏览器卡死

同步:按顺序执行,执行完一个执行下一个

但是渲染主线程还有很多重要的事要做,那么多任务等着,可不敢阻塞!所以浏览器使用异步解决这个问题

异步:彼此独立执行,等待事件的同时继续做别的事

页面卡顿

有这样一段代码

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>114514</h1>
<button>change</button>
<script>
const h1 = document.querySelector('h1')
const btn = document.querySelector('button')

// 进行一段时间的死循环
const delay = (duration)=> {
const start = Date.now()
while(Date.now() - start < duration) {}
}

btn.onclick = ()=> {
h1.innerText = '1919810'
delay(3000)
}
</script>
</body>
</html>

结果是,点击按钮三秒后标题才会改变

消息队列优先级

任务没有优先级,但消息队列本身是有优先级的

根据W3C标准:

  • 每个任务都有一个任务类型,同一类型的任务必须属于同一队列、不同类型的任务可以分属不同队列(队列可处理不同类型的任务),在一次事件循环中,浏览器可根据实际情况从不同的队列中取出任务执行
  • 浏览器必须准备一个微队列,其中的任务优先于所有其他任务的执行HTML Standard

而chrome浏览器中包含这些队列:

  • 延时队列:存放计时器到时的回调任务,优先级【中】

  • 交互队列:存放用户操作后的事件处理任务,优先级【高】

  • 微队列:存放需要最快执行的任务,优先级【最高】

    添加任务到微队列的主要方式是使用Promise或者MutationObserver

    1
    2
    // 其实就是立即把一个函数添加到微队列
    Promise.resolve().then(fn)
  • 等等等等

以前的标准为两个消息队列:宏队列(普通通道)和微队列(VIP通道),但已不适用于现在日渐复杂的浏览器

1
2
3
4
5
setTimeout(() => {
console.log(1)
}, 0)

console.log(2)
答案

2

1

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

// 进行一秒死循环
delay(1000)

console.log(2)
答案

(停顿一秒)

2

1

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

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

console.log(3)
答案

3

2

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const a = ()=> {
console.log(1)
Promise.resolve().then(()=> {
console.log(2)
})
}

setTimeout(() => {
console.log(3)
Promise.resolve().then(a)
}, 0)

Promise.resolve().then(()=> {
console.log(4)
})

console.log(5)
答案

5

4

3

1

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 答案:4 1 2 3
const a = ()=> {
console.log(1)
Promise.resolve().then(()=> {
console.log(2)
})
}

setTimeout(() => {
console.log(3)
}, 0)

Promise.resolve().then(a)

console.log(5)
答案

4

1

2

3

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
49
50
51
52
53
54
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<!-- 条件:点击begin后的四秒内点击interaction -->
<button id="begin">开始</button>
<button id="interaction">添加交互任务</button>
<script>
const begin = document.querySelector('#begin')
const interaction = document.querySelector('#interaction')

// 进行一段时间的死循环
const delay = (duration) => {
const start = Date.now()
while (Date.now() - start < duration) { }
}

// 往延时队列添加任务
const addDelay = () => {
console.log('添加延时任务')
setTimeout(() => {
console.log('执行延时任务')
}, 100)
// 此处开启死循环的目的:确保该函数执行完后任务已入队
delay(2000)
}

// 往交互队列添加任务
const addInteraction = () => {
console.log('添加交互任务')
interaction.onclick = () => {
console.log('执行交互任务')
}
// 同上~
delay(2000)
}

begin.onclick = () => {
// 先给延时队列添加任务
addDelay()
// 再给交互队列添加任务
addInteraction()
console.log('begin...')
}
</script>
</body>

</html>
答案

添加延时任务

添加交互任务

begin…

执行交互任务

执行延时任务