上文提到,浏览器多进程模型中,渲染进程负责将我们编写的代码渲染成画面,渲染进程的主线程称为渲染主线程。
为了将 html、css 等代码渲染成我们看到的像素,渲染主线程要处理非常多的任务,它的工作包含:
- 解析 html、css 代码
- 计算样式、布局,样式变化时重新计算
- 调用 V8 引擎执行 JavaScript 代码
- 事件发生时,执行回调函数
- ...
这些任务之间并非独立的,比如 JS 可以修改 css 样式,在计算样式时不能执行 JS 代码,因此无法用多个线程处理。
渲染主线程为了调度这些任务,创建了一个消息队列。所谓事件循环,其实就是渲染主线程基于消息队列的一种任务调度方式。
事件循环的工作方式很简单,简单来说就是一句话:任务进入消息队列中排队,渲染主线程从消息队列中取出任务执行。具体来说,渲染主线程处于一个循环中,它不断去队列中查看是否有任务,如果有,则取出执行,执行完继续查看队列;如果没有任务,则进入休眠状态,直到有下一个任务进入队列。
其中,每个任务又可以创建新的任务进入队列,其他线程也可以创建任务。总之,凡是需要渲染主线程执行的任务,通通去排队。
任务的类型有很多,比如计时器的回调函数,网络资源到达事件,用户点击事件的回调等等。在过去,上图的消息队列被称为宏任务队列,它管理着所有任务的调度。
现今,单个队列已无法满足复杂任务的调度。于是,新的标准规定:
- 浏览器可以创建多个队列,根据实际情况从队列中取出任务执行。
- 浏览器必须提供一个微队列,微队列中的任务优先所有其他任务执行
在 chrome 的实现中,包含以下几个队列。
- 微任务队列:用户存放需要最快执行的任务,优先级「最高」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
不难看出,chrome 认为响应用户交互更重要,因此,交互队列的优先级要高于延时队列。
需要注意的是,同类型的任务必须在一个队列,不同类型任务可以分属于不同的队列。也就是说,并非有多少种类型的任务就要创建多少个队列,只要保证同一类型的任务不要混乱地分散在各个队列中即可。
最后我们简单地来看下微任务队列。
在 JavaScript 中,添加任务到微队列的主要方式是使用 Promise、MutationObserver、queueMicroTask。
每次事件循环时,只要微队列中有任务存在,必须优先执行微任务,微队列清空后,浏览器再根据自身对各任务队列的优先级理解,从优先级高的队列中取任务执行。