浏览器事件循环

657 阅读7分钟

为什么需要事件循环?事件循环到底解决了什么问题?浏览器的事件循环不是js单方面的技术,我们要知道前因后果还需要了解浏览器进程线程相关的知识

浏览器进程模型

进程

程序运行需要自己专属的内存空间,可以把这块内存空间简单的理解为进程,每个应用至少有一个进程,进程之间相互独立,即便要通信也需要双方同意。这样设计的目的就是为了隔离,即使一个程序崩溃了也不会影响其他程序。

线程

一个进程至少有一个线程,线程就是来运行代码的。进程在开启之后就会自动创建一个线程来运行代码,这个线程就是主线程

如果程序同时需要运行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中是可以包含多个线程的

浏览器有哪些进程和线程

浏览器内部工作极其复杂,已经接近操作系统了,为了避免相互影响,减少连环崩溃的几率,比如网络进程崩溃了不会影响渲染进程等等。当浏览器启动后,会自动启动多个进程,浏览器是一个多进程多线程的应用程序

可以在浏览器的任务管理器中查看当前的所有进程

image.png

其中最主要的进程有下面三个:

  1. 浏览器进程,浏览器进程内部会启动多个线程处理不同任务,主要负责以下任务
    • 界面展示(不是我们开发的页面)比如浏览器标签页,导航栏,前进后退等等;
    • 用户交互,监听用户操作,比如用户点击,键盘事件,鼠标滚轮,拖动了滚动条等等
    • 子进程管理等等。
  2. 网络进程,负责加载网络资源,网络进程内部会启动多个线程来处理不同的网络任务
  3. 渲染进程(这里才是我们真正需要了解的地方),渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML,CSS,JS代码,默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同标签页之间互不影响,所以chrome浏览器是内存杀手。(新版本可能会做一些优化,比如一个站点一个进程,这样就能减少渲染进程,减小内存损耗)

渲染主线程

事件循环实际上就发生在渲染主线程

渲染主线程是浏览器中最繁忙的线程,需要处理很多任务

  • 解析HTML
  • 解析CSS
  • 计算样式
  • 布局
  • 处理图层
  • 渲染,每秒把页面画60次
  • 执行全局js代码
  • 执行事件处理函数
  • 执行计时器回调函数
  • ...

为什么渲染进程不适用多个线程来处理这么多事情呢?

这里问题就来了,处理这么多任务,那要如何调度呢,比如

  • 一个js函数正在执行,用户点击了按钮,此时应该执行点击事件处理函数吗
  • 一个js函数正在执行,某个计时器到达了时间,应该立即执行回调吗
  • 用户点击了按钮,同时计时器也到了时间,应该处理哪一个呢
  • ...

这时浏览器就使用一个绝妙的主意来处理这个问题:排队。既然很忙,那就维护一个队列挨个来完成任务

image.png

  1. 在最开始的时候,渲染主线程会开启一个无限循环
  2. 每一次循环都会检查消息队列中是否有其他任务存在。如果有,就会取出第一个任务执行,执行完成之后进入下一次的循环,如果没有,就会进入休眠状态
  3. 其他的所有线程可以随时向消息队列中添加任务。新任务会加到消息队列的末尾。再添加新任务的时候,如果主线程是休眠状态就会将其唤醒继续循环拿取任务

这一整个过程,就成为事件循环

相关概念

异步

代码在执行过程中,会遇到一些无法处理的任务,比如

  • 计时完成后需要执行的任务 --- setTimeout\setInterval
  • 网络通信完成后需要执行的任务 --- XHR\Fetch
  • 用户操作后需要执行的任务 --- addEventListener

如果让渲染主线程等待这些任务的实际到达,就会导致主线程长期处于【阻塞】状态,从而导致浏览器卡死

image.png

主线程是很忙的,还要处理其他任务,尤其是还要处理渲染任务,如果阻塞页面就会直接卡死一段时间

因此浏览器就采用异步来解决,交给计时器线程后就直接结束,开始从消息队列中取下一个任务执行了,计时器结束以后再往消息队列中添加任务,使用这种方式,主线程就永远不会阻塞

image.png

如何理解js的异步

js本身是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。渲染主线程承担着诸多的工作,比如渲染页面,执行js等等

如果使用同步方式,极有可能造成主线程产生阻塞,从而导致消息队列中的很多任务无法得到及时执行,这样一来,一方面导致繁忙的主线程白白的消耗时间,另一方面页面渲染任务无法及时执行,导致页面无法及时更新形成卡顿

所以浏览器采用异步的方式,遇到某些任务时,比如计时器,网络,事件监听,主线程会将任务交给其他线程处理,自身立即借宿任务的执行,转而执行后续代码,当其他线程完成时,将事先传递的回调函数,包装成任务,加入到消息队列中排队,等待主线程的调用

JS为什么会阻碍渲染

当我们点击按钮之后会发现,过了3秒钟,页面文字才会发生变化,为什么呢

<h1>哈哈哈哈</h1>
<button>修改</button>
<script>
    let h1 = document.querySelector('h1')
    let btn = document.querySelector('button')

    function delay(duration) {
      let start = Date.now()
      while(Date.now() - start < duration) {}
    }

    btn.onclick = function () {
      h1.textContent = 'hello world'
      delay(3000)
    }
</script>

image.png

btn.onclick会交给事件监听线程,全局js执行完毕就直接从主线程中移除了,此时没有其他任务主线程就进入休眠

image.png

回调中产生了一个绘制任务,所以要等回调执行完成之后(注意这里就会有问题了,我们渲染主线程被delay函数长时间占用,所以会阻塞3秒)才会从消息队列中取绘制任务

任务有优先级吗

任务是没有优先级的,但是在消息队列是有优先级的

这个其实和原来的理解是有偏差的

在以前我们通常认为任务是分宏任务队列和微任务队列的,但是这种模式已经满足不了浏览器的需要了,随着浏览器复杂度的不断提升,W3C不再使用宏队列的说法了

W3C的最新解释

每个任务都有一个任务类型,同一个类型的任务必须在一个队列中,不同类型的任务可以出现在不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行

浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务的执行

比如chrome中的实现

  • 延时队列(不太重要,优先级中)
  • 交互队列(重要,优先级为高)
  • 微任务队列(最重要,优先级为最高)
  • 等等

js中计时器能做到精确计时吗,为什么?

不能精确计时的

  • 计算机硬件没有原子钟,无法做到精确计时
  • 操作系统的计时函数本身就有少量偏差,由于js的计时器最终调用的是操作系统的函数,也就带有一定的偏差
  • 浏览器在实现计时器时,如果嵌套层级超过5层就会带来4毫秒的误差
  • 受事件循环的影响,计时器的回调函数只能在主线程空闲的时候运行,这就又带来了偏差