一、理论体系背景
(一)问题域
事件循环(Event Loop)大家应该并不陌生,它是前端极其重要的基础知识。在平时的讨论或者面试中也是一个非常高频的话题。随着W3C规范的修改,各大浏览器也对事件循环进行相应修改。
根据 W3C 的最新解释:每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行。
随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法
下面涉及的代码示例,我将以谷歌浏览器为例进行演示。
(二)术语体系
1.进程和线程
- 进程:工厂
- 线程:工人
进程:进程是操作系统分配的最小基本单位,进程中包含线程
必须是运行中的某个应用程序才能称得上是进程,也就是应用关闭,进程自然也寄了(进程是动态概念)
线程:线程由进程所管理。为了提升浏览器的稳定性和安全性,浏览器采取了多进程模型。
- 线程是进程的基本单位,一个进程由一个或多个线程组成,那么线程就是程序执行的最小单元(CPU调度的最小单位)
- 线程也是一个动态的概念,存在是暂时的而非永久的
- 进程与线程的区别:进程在运行的时候拥有独立的内存空间,也就是说每个进程占用的内存都是独立的
- 多个线程是共享内存空间的,但是每个线程的执行是相互独立的,线程必须依赖于进程才能执行,单独的线程无法执行,由进程控制线程的执行,没有进程就不存在线程
2.浏览器是多进程的
浏览器打开一个网页(Tab)相当于新起一个进程(每个进程内有自己的多线程)
3.浏览器的进程有什么
-
渲染进程
-
负责将HTML,CSS,JS转化为可以用户可与之交互的网页,排版殷勤Blink和JavaScriptV8都运行在这个进程里,默认情况下,Chrome为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程跑在沙箱环境之下
-
浏览器进程
负责页面的展示以及交互
-
网络进程(下载进程)
负责页面网络资源的加载,之前作为一个模块运行在浏览器里,现在才成为一个单独的进程
-
GPU进程
GPU的意思是:图形处理器,用于负责CSS 3D效果,网页,chorme UI的绘制
-
插件进程
-
负责插件的允许,插件容易崩溃,我们插件进程处理使得插件崩溃浏览器不崩溃
4.渲染主线程
(1)第一版线程模型
●如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务
第一版:线程的一次执行
(2)第二版线程模型
●要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统
第二版:在线程中引入事件循环
(3)第三版线程模型
●消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
●如果要接收其他线程发送过来的任务,就需要引入消息队列。
分为下面三个步骤:
-
添加一个消息队列;
-
IO 线程中产生的新任务添加进消息队列尾部;
-
渲染主线程会循环地从消息队列头部中读取任务,执行任务。
第三版线程模型:队列 + 循环
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画 60 次
- 执行全局 JS 代码
- 执行事件处理函数
- 执行计时器的回调函数
- …
思考题:为什么渲染进程不适用多个线程来处理这些事情?
要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务?
比如:
- 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
- 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
- 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
- …
渲染主线程想出了一个绝妙的主意来处理这个问题:排队
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
这样一来,就可以让每个任务有条不紊的、持续的进行下去了。
整个过程,被称之为事件循环(消息循环)
(4)谷歌消息队列
JS为何会阻碍渲染?
先看代码
<h1>hello world!!!</h1>
<button>change</button>
<script>
const h1 = document.querySelector('h1');
const btn = document.querySelector('button');
// 死循环指定的时间
function delay(duration) {
const start = Date.now(