深入理解浏览器的事件循环
浏览器的进程模型
- 浏览器进程:负责子进程的管理和用户交互
- 网络进程:负责加载网络资源
- 渲染进程:浏览器会为每一个标签页开启一个新的渲染进程。
- 渲染进程中的渲染主线程是我们最关注的,因为浏览器的事件循环就发生在这之中
- 渲染主进程不仅负责页面的渲染,还负责 JS 脚本的执行
- 除此之外,渲染进程中还有交互线程,处理定时器的线程等
[!NOTE]
但是每个标签开一个渲染进程,会浪费太多的内存,因此 Chrome 浏览器在尝试以另外的模式(同一个主域名下的子网页共用一个渲染进程)来优化浏览器系统的性能。
如何理解 JS 的异步?
因为 JS 一门单线程语言,他执行在渲染主线程中,当我们遇到一些任务需要长时间等待时,比如定时器,如果他是同步的话,必须等待计时器结束,然后执行定时器的回调之后才能继续执行后续的任务,这样会白白等待太多时间,因此渲染主进程会将异步任务丢给其他线程去处理(比如定时器会调用操作系统来辅助操作),当其他线程的异步任务执行完后,会将回调包装成任务放到消息队列的末尾,等待渲染主线程去调度。
事件循环整体流程:
- 浏览器的主线程会进入无限的循环监听(不会结束的
for
循环) - 首先检查调用栈是否为空
- 调用栈不为空则先执行当前调用栈中的任务
- 调用栈为空的话去消息队列中取所有的微任务来执行
- 执行完所有的微任务后,取一个宏任务来执行
- 接着循环上述过程
保证浏览器永不阻塞,最大程度保证单线程的流畅执行。
[!WARNING]
但是随着任务类型的愈加复杂,W3C 已不再采用宏任务的说法,而是将异步任务分为很多不同的类型,同一个类型的任务必须放在同一个队列,例如 Chrome 浏览器中将异步任务队列分为:
- 延时队列:计时器到达后执行的回调任务,优先级 [中]
- 交互队列:用户交互后产生的事件处理任务,优先级 [高]
- 微队列:如 Promise、MutationObserver、queueMicroTask,优先级 [最高]
如何理解 JS 会阻塞渲染?
其实浏览器的渲染任务也是异步的,如何在执行 JS 脚本的过程修改了 DOM 的信息,那么浏览器也会把这个渲染任务放到消息队列里,等待事件循环机制取调度它。
为什么计时器不能做到精准计时?
受事件循环的影响,计时器的回调函数只能在渲染主线程空闲时运行,主线程在运行时可能有长时的任务,或者其他优先级更高的消息队列先执行完才能去调用延时队列中的计时器任务。