事件循环又称消息循环。
均以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 | // Promise.resolve().then(fn) 会将函数fn加入微队列 |
Q:浏览器执行以上js代码后输出是什么?
解析:
setTimeout
将函数fn3
添加到计时线程,0秒后时间到,计时线程将fn3
添加到延时队列中排队等待调度;- 全局js还未执行完,主线程继续执行,将
fn4
添加到微队列; - 输出
5
; - 全局js执行完毕,从消息队列中取出下一个任务。由于微队列优先级最高,因此取出
fn4
执行,输出4
; fn4
执行完毕,取出fn3
执行,输出3
,然后将fn1
加入微队列;fn3
执行完毕,取出fn1
执行,输出1
,然后将fn2
加入微队列;fn1
执行完毕,取出fn2
执行,输出2
;
A:
1 | 5 |