浏览器进程模型
在理解什么叫事件循环前,我们需要先知道浏览器的进程模型
现代浏览器的功能极度复杂,为了能确保各个部分独立运行互不影响,浏览器会在启动之时开启多个进程
,具体而言可以分为以下三种
浏览器进程
负责浏览器的用户交互,子进程管理,浏览器页面显示等等,浏览器进程是最先被启动的进程,将由它来启动并维护其他进程
网络进程
负责浏览器的网络通信
,加载网络资源,会在内部开启多个线程来维护不同的网络任务渲染进程
渲染进程不同于其他进程,它一次只能开启一个线程
,这个线程也被称为渲染主线程
,将由它负责渲染页面,执行js等等
默认情况下,现代浏览器都是开启一个页面则开启一个渲染进程
,具体可以通过浏览器的任务管理器来查看
渲染主线程
渲染主线程
可以说是浏览器中最繁忙的线程,因为各种各样的原因导致渲染进程一次只能开启一个渲染主进程
,所以主线程将负责包括但不限于以下内容
- 解析HTML
- 解析CSS
- 执行JS代码
- 处理图层
- …
可以看到渲染主线程的任务多且杂,但渲染主线程只有一个,一次只能执行一个任务,那任务与任务之间又如何调度
呢
渲染主线程的解决方案是将任务排队
消息队列
渲染主线程会维护消息队列
,所有任务都需要按照先来后到
的顺序放入消息队列
中,渲染主线程则会依次取出
消息队列
中的任务执行,具体有以下步骤
- 最开始
渲染主线程
会开启一个死循环
- 每一次
循环渲染主线程
都会检测消息队列
中是否有任务,如果有就取出执行
,没有就进入休眠
状态 - 其他线程可以随时向消息队列中
添加任务
,如果此时渲染主线程
处于休眠
状态则会被唤醒
基于这样一个循环,我们的渲染主线程就能有条不紊的执行下去了
异步
到现在为止一切似乎都没什么问题,任务在消息队列的调度下能有序执行
,最大的困难便解决了,但任务不全都是同步执行
,有些任务浏览器遇到了之后并不立刻执行,如setTimeOut
,setInterval
,Promise
等等,如果直接将其当做同步任务
执行的话就会导致页面阻塞
为了保证用户的使用体验,使页面不被卡死,因此浏览器采用异步
的方式来解决这个问题,具体步骤如下
- 当渲染主线程遇到
异步任务
时会通知对应的线程
,当前任务结束
,从消息队列取出任务继续执行
- 其他线程开始
监控
,待任务到达触发时机就会将任务的回调函数包装成任务放入消息队列中
渲染主线程
会自始至终从消息队列中取出任务执行
当然,所有异步任务
不可能都由一个线程处理,不同的异步任务会有对应的线程接管
浏览器通过使用异步的形式来使渲染主线程不会出现阻塞
由此我们能得到两个结论
单线程是异步产生的原因
事件循环是异步的实现方式
任务优先级
到了这里,似乎事件循环我们已经了解的差不多了,但还有一些疑问我们仍未解决,任务与任务之间有优先级的区别吗
事实上,任务与任务之间并没有优先级
的区别,都是先进先出
,但消息队列与消息队列间有着不同的优先级
消息队列
向来都不只有一个,具体可以分为宏队列
和微队列
然而随着现代浏览器复杂度的急剧提升,原有的划分方式已不满足于现在需要,所以目前的消息队列
划分如下
- 每个任务都会有一个
任务类型
,同一个类型的任务必须在同一个队列
,不同类型的任务可以在同一个队列
- 每个浏览器都必须拥有一个
微队列
,微队列里的任务优先于其他队列里的任务执行
- 其他队列里的
任务
浏览器可以按照实际情况执行
事件循环
最后,我们再来总结一下什么是事件循环
事件循环是浏览器渲染主线程的主要工作方式,因为各种各样的原因,浏览器开辟的渲染进程只会存在一个渲染主线程,这也就决定js是一门单线程的语言,同时渲染主线程负责的任务十分繁杂,包括渲染HTML,CSS,执行JS,每秒重绘多少多少次,布局等等,为了保证所有任务都能稳定有序执行,浏览器会维护消息队列来存放任务,其他线程可以向消息队列中提交任务,渲染主线程每次都会从消息队列中取出第一个任务来执行,如果没有任务渲染主线程就会进入休眠,待消息队列中有了新的任务就会被唤醒,如果是异步任务则会交由其他线程托管,待任务需要执行时将事件的回调函数包装成任务放入消息队列中。根据W3C规定,每个任务都有一个消息类型,同一类型的任务必须要在同一个消息队列中,不同类型的任务可以在一个消息队列中,每个浏览器必须要有一个微队列,并且微队列中的任务要先于其他消息队列中的任务执行。
关于JS中能否做到精准计时
严格上来说是不能
的,主要原因有以下几点
- 计算机内没有
原子钟
,无法做到精确计时 - js的计时也是调用
系统级的函数
,会有些许偏差 - W3C规定计时器嵌套超过4层的话从
第5层开始强制计时器有5ms延时
- 当计时器结束,任务进入消息队列也不是
立刻执行
的,需要等待渲染主线程的调用
,这也会有时间上的偏差