浏览器渲染进程模型——事件循环

149 阅读4分钟

何为进程?程序运行需要它自己专属的内存空间,可以简单将其理解为程序的进程。
何为线程?一个进程至少有一个线程,即主线程,不同线程分别处理不同的任务。

一、浏览器的进程分类

浏览器是一个多进程多线程的应用程序,主要分为浏览器进程、网络进程、渲染进程

二、渲染进程

渲染进程中的渲染主线程要处理的任务包括但不限于:

  1. 解析 HTML、CSS
  2. 计算样式
  3. 布局
  4. 分层
  5. 生成绘制指令
  6. ...
  7. 执行全局 JS 代码
  8. 执行事件处理函数
  9. 执行计时器的回调函数
  10. ...

详见《组织严密的生产流水线——浏览器的渲染原理》

三、渲染主线程的工作原理

渲染主线程要处理这么多任务,但由于 JS 的执行过程和用户 UI 交互产生的新任务等复杂的任务调度难题,队列模式由此产生

image.png

  1. 最开始渲染主线程进入无限循环
  2. 每次循环会检查消息队列中是否有任务,有则取出执行,执行完进入下个循环(nextTick),没有任务则休眠
  3. 其他所有线程(包括其他进程内的线程)都可随时向消息队列添加任务,新任务排在队列末尾。在添加新任务时,如果主线程在休眠,则会将其唤醒以继续循环。

通过以上模式就可以让每个任务都有条不紊的进行下去了,这整个过程就称为事件循环(谷歌浏览器源码称为消息循环

四、同步、异步

主线程上的JS引擎把同步任务放入执行栈中,执行完毕后再执行消息队列里的任务,但在代码执行过程中,会遇到一些无法立即处理的任务,如:

  • 计时器完成后需要执行的任务:setTimeout、setInterval
  • 网络通信完成后需要执行的任务:XHR、Fetch
  • 用户操作后需要立即执行的任务:addEventListener

因为JS是单线程的,且渲染主线程也只有这一个,如果让渲染主线程等待这些任务执行完后再执行其他任务(同步),那就会导致主线程的阻塞状态,阻碍了页面渲染等任务,从而导致当前页面卡死。

因此浏览器选择异步解决此类问题,即当这些无法立即处理的任务交给其他线程(宿主环境提供,如浏览器、node)去处理,等到了执行时机时,再将这些任务的回调函数包装成任务放到消息队列末尾,再利用主线程事件循环机制去有序执行。

总结:单线程是异步产生的原因,事件循环是异步实现的方式

早期JS把异步任务还分为微任务和宏任务,随着浏览器复杂性的提升,W3C 已不再使用微任务与宏任务这种简单说法,队列也从微、宏双队列改为了区分任务类型的多队列模式

五、任务的优先级、微队列

任务没有优先级、在单个消息队列中先进先出,但所有的消息队列有优先级,W3C 最新的解释:

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

在目前的谷歌浏览器中,至少包含了以下队列:

  • 微队列:存放需要最快执行的任务,优先级【高】(W3C 规定)
  • 交互队列:存放用户操作后产生的事件处理任务,优先级【中】(用户体验好)
  • 延时队列:存放计时器到达后的回调任务,优先级【低】

添加任务到微队列的主要方式是使用PromiseMutationObserver

// 立即把一个函数添加到微队列
// 其中 Promise 本身是同步任务,而 then/catch 的回调函数则是异步的,执行顺序大有不同
new Promise((resolve) => {
    同步任务
    resolve().then(函数)
})

//当监测的 DOM 发生变动时,MutationObserver 将收到通知并触发事先设定好的回调函数
var observer = new MutationObserver(function(mutationsList, observer) {
    处理变动
});