浏览器事件循环

事件循环又称消息循环。

均以Chrome浏览器为例。

浏览器的进程模型

浏览器运行时有很多个进程,我们重点关注三大进程:

  • 浏览器进程
  • 网络进程
  • 渲染进程(不止一个,每一个标签页就是一个渲染进程)

其中浏览器进程是打开浏览器后最先启动的,负责:

  • 浏览器通用界面显示,如浏览器工具栏、浏览器设置等
  • 用户交互,如监听点击、滚动等
  • 子进程管理,浏览器其他进程均由浏览器进程开启
  • ……

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

渲染进程可以有多个,每一个标签页就是一个渲染进程,该进程包括一个渲染主线程和其他多个子线程。

渲染主线程是如何工作的

渲染主线程的任务包括但不限于:

  • 解析HTML
  • 解析CSS
  • 计算样式,如把rem转换为px
  • 计算布局,如计算元素宽高
  • 处理图层,如根据z-index判断叠层关系
  • 每帧绘制页面
  • 执行JavaSctipt
  • 执行事件处理函数
  • 执行计时器的回调函数
  • ……

主线程每次执行一个任务,执行完后从消息队列中取出下一个任务。所有线程可以随时向消息队列中添加任务。这整个过程称为事件循环

若是需要等待的任务,如setTimeout()函数会在等待规定时间后才执行,则不能直接加入消息队列,否则会导致主线程要等待,而后面的任务也无法执行。

主线程执行到js中的setTimeout(),会把要计时等待的任务交给计时线程,由计时线程到时间后将回调函数加入消息队列。

Q:如何理解JS的异步?

A:

JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
渲染主线程承担着诸多的工作,渲染页面、执行JS都在其中运行。
如果使用同步的方式,就极有可能导致主线程阻塞,从而导致消息队列中的很多其他任务无法得到执行。
这一方面会导致繁忙的主线程白白消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
所以浏览器采用异步方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器永不阻塞,最大限度保证了单线程的流畅运行。

任务的优先级

实际上消息队列不止一个队列,而是有多个队列:微队列延时队列交互队列等。

在W3C标准中,规定微队列是优先级最高的,意味着只有当微队列中所有任务都执行完了才会轮到其他队列。

在Chrome浏览器中,延时队列用于存放计时器到达后的回调函数,优先级为【中】;交互队列用于存放用户操作后产生的事件处理任务,优先级为【高】。

Q:阐释一下JS的事件循环?

A:

事件循环又叫消息循环,是浏览器渲染主线程的工作方式。
在Chrome中,它开启一个不会结束的循环,每次循环从消息队列中取出第一个任务执行,而其他线程只要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列微队列,这种方式已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据W3C官方的规定,每个任务有不同的类型,同类型的任务必须在同一个队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务优先级最高,必须优先调度。

Q:JS中的计时器能做到精确计时吗?为什么?

A:

不行。
受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,回调函数需要时间排队等待调度,因此带来了时间偏差。
操作系统的计时函数本身就有少量偏差,而JS的计时器最终调用的是操作系统的计时函数,因此也具有一定偏差。

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Promise.resolve().then(fn) 会将函数fn加入微队列

function fn1() {
console.log(1)
Promise.resolve().then(function fn2() {
console.log(2)
})
}

setTimeout(function fn3() {
console.log(3)
Promise.resolve().then(fn1)
}, 0)

Promise.resolve().then(function fn4() {
console.log(4)
})

console.log(5)

Q:浏览器执行以上js代码后输出是什么?

解析:

  1. setTimeout将函数fn3添加到计时线程,0秒后时间到,计时线程将fn3添加到延时队列中排队等待调度;
  2. 全局js还未执行完,主线程继续执行,将fn4添加到微队列;
  3. 输出5
  4. 全局js执行完毕,从消息队列中取出下一个任务。由于微队列优先级最高,因此取出fn4执行,输出4
  5. fn4执行完毕,取出fn3执行,输出3,然后将fn1加入微队列;
  6. fn3执行完毕,取出fn1执行,输出1,然后将fn2加入微队列;
  7. fn1执行完毕,取出fn2执行,输出2

A:

1
2
3
4
5
5
4
3
1
2