浏览器工作原理(15) - 消息队列和事件循环

上一篇文章讲了V8引擎编译器和解释器,了解了V8是如何执行一段代码的,接下来的文章再着重分析一下浏览器中的页面循环系统

浏览器每个渲染进程都有一个主线程,主线程负责处理DOM,计算样式,处理布局,还需要执行JavaScript代码以及交互,显得非常的繁忙。那么这么多的任务,主线程是怎么按顺序执行的呢?这时候就要有一个系统来专门负责调度这些任务,这个系统就是今天的主角 – 消息队列和事件循环系统

使用单线程处理安排好的任务

比如有一下几个任务:

  • 任务 1:1+2
  • 任务 2:20/5
  • 任务 3:7*8
  • 任务 4:打印出任务 1、任务 2、任务 3 的运算结果

把所有任务代码按照顺序写进主线程里,等线程执行时,这些任务会按照顺序在线程中依次被执行;等所有任务执行完成之后,线程会自动退出。可以参考下图来直观地理解下其执行过程:
浏览器工作原理(15) - 消息队列和事件循环

在线程运行过程中处理新任务

但并不是所有的任务都是在执行之前统一安排好的,大部分情况下,新的任务是在线程运行过程中产生的。比如在线程执行过程中,又接收到了一个新的任务要求计算“10+2”,那上面那种方式就无法处理这种情况了。

要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制

可以通过一个 for 循环语句来监听是否有新的任务

这次的执行机制中,我们加入了一个循环,另外还引入了另一个事件(等待输入并执行),通过引入事件循环机制,就可以让该线程“活”起来了,我们每次输入两个数字,都会打印出两数字相加的结果,你可以结合下图来参考下这个改进版的线程
浏览器工作原理(15) - 消息队列和事件循环

处理其他线程发过来的任务

之前的示例,所有任务都来自线程内部,如果另外一个线程想要让主线程执行一个任务,那么该怎么办呢?一起看一下其他线程是如何发送消息给渲染主线程的,参考下图:
浏览器工作原理(15) - 消息队列和事件循环
从上图可以看出,渲染主线程会频繁接收到来自于 IO 线程的一些任务,接收到这些任务之后,渲染进程就需要着手处理,比如接收到资源加载完成的消息后,渲染进程就要着手进行 DOM 解析了;接收到鼠标点击的消息后,渲染主线程就要开始执行相应的 JavaScript 脚本来处理该点击事件。

那么如何设计好一个线程模型,能让其能够接收其他线程发送的消息呢?一个常用模式就是使用消息队列

消息队列

什么是消息队列?
浏览器工作原理(15) - 消息队列和事件循环
消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。

现在改变一下上面的执行方式:

浏览器工作原理(15) - 消息队列和事件循环
从上图可以看出,我们的改造可以分为下面三个步骤:

  1. 添加一个消息队列;
  2. IO 线程中产生的新任务添加进消息队列尾部;
  3. 渲染主线程会循环地从消息队列头部中读取任务,执行任务。
处理其他进程发送过来的任务

现在我们实现了跨线程执行任务,那么如何处理跨进程的任务呢?

浏览器工作原理(15) - 消息队列和事件循环
从图中可以看出,渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续的步骤就和前面讲解的“处理其他线程发送的任务”一样了