跳转到内容

事件循环

刀刀

3/29/2025

0 字

0 分钟

要讲事件循环,首先要把它前因后果都要讲清楚:浏览器的进程模型。因为这个事件循环跟浏览器是密切相关的,浏览器要执行JS,它的关联是浏览器里边的东西,它是跟浏览器密切相关的。了解浏览器,要讲这个浏览器的进程模型。

浏览器进程模型

进程

啥叫进程?

一个程序,像QQ,微信,王者荣耀,他们都属于程序,程序运行需要啥,它需要内存空间。程序的变量、对象、函数等,这些东西都是放到内存里边的,所以它需要有一块自己专属的内存空间,不够了,再给扩容。

于是程序运行它需要有一块专属内存空间,可以把简把进程简单的理解为就是这块内存空间,有这块内存空间就是进程,那么每一个程序呢,一启动的时候至少得有一个进程,也就是至少得有一块空间,而且进程与进程之间是相互独立的,就内存空间之间是隔离的。

隔离的好处在,如果QQ自己出了问题了,比方内存空间里面乱了套了,自己崩溃了,不会影响到微信。因此进程之间它是相互隔离的,每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。

线程

进程搞清楚了之后呢,接着来看线程,这两个玩意就是密不可分的。

运行代码的那个「人」,可以把它简单的称为线程,所以说一个进程,它至少得有一个线程,不然的话分配块内存空间,又不干活,不中用,就会把这个进程给杀死,把进程退出进程叫做杀死进程,所以呢,一个进程至少得有一个线程去干活,去运行代码,因此在进程启动的时候,就会自动的创建一个线程来运行代码,那么这个线程呢,我们把它称之为主线程,就是跟随着进程启动的时候产生的线程。

但是很多程序呢,它比较复杂,它可能需要同时的执行多块代码,比方说王者荣耀,他要监听各种操作,要让人物的跟随的操作来移动,还要进行网络通信,都是需要网络的,这么多事情要做,可能一个人呢,他忙不过来,于是呢,主线程,他忙不过来的时候,他就会想办法,多招点人,会启动更多的线程来执行代码。一个进程可以包含多个线程。

进程和线程

浏览器是一个多进程多线程的应用程序,内部工作极其复杂,为了避免相互影响,为了减少连环崩溃几率,当启动浏览器后,它会自动启动多个进程。其中一个进程崩溃了,也不会影响到另外的进程。

主要分为三个进程:

  • 浏览器进程

    主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。

  • 网络进程

    负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务

  • 渲染进程

    渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。

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

渲染主线程是浏览器最繁忙的线程,需要它处理的任务包括但不限于:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画60次(FPS 帧率)
  • 执行全局 JS 代码
  • 执行事件处理函数
  • ......

如果主线程遇到一个任务,该如何调度?

比如:

  • 在执行 JS 函数时,用户点击了一个按钮,应该立即执行这个点击函数吗?
  • 在执行 JS 函数时,某个计时器时间到了,应该立即执行这个计时器函数吗?
  • 在执行用户点击按钮时,某个计时器时间到了,应该先执行点击函数还是计时器函数?

渲染主线程想出了一个新的方法:排队。主要做以下操作:

  1. 在最开始的时候,渲染主线程会进入一个无限循环

    渲染主线程的启动源码是用 C++ 写的,他是这么写的:

    c++
    for( ;; ) {
      Delegate::NextWorkInfo next_work_info = delegate->DoWork();
      // ....
    }

    条件永远为真,就能进入无限循环。他每一次循环做了很多事情,如从消息队列里拿下一个任务。

  2. 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环。如果没有,则进入休眠状态

  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务,新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒继续循环拿去任务

TIP

整个过程,被称之为事件循环,也被称之为消息循环。

其他细节

事件循环到此已经说明完毕了,但还是不够,事件循环里面的细节也需要解释一下,事件循环会有更多的理解。

异步

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

  • 计时完成后需要执行的任务 —— setTimeoutsetInterval
  • 网络通信完成后需要执行的任务 —— XHRFetch
  • 用户操作后需要执行的任务 —— addEventListener

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

渲染主线程承担着及其重要的工作,无论如何不能阻塞!

因此,浏览器选择异步来解决这个问题。使用异步方式,渲染主线程永不阻塞

如何理解 JS 异步?

JS 是单线程语言,这是因为它运行在浏览器的渲染主线程中。而渲染主线程只有一个。

但是渲染主线程承担很大工资,如渲染页面、执行 JS 操作等,如果使用同步的方式,就有可能造成主线程阻塞,导致消息队列中很多任务无法得到执行。一方面导致繁忙的主线程白白消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。

所以浏览器采用异步的方式来避免,具体做法是当某些任务发生时,比如定时器、事件监听等,主线程将任务交给其他线程执行处理,自身立即结束任务执行,转而执行后续代码。当其他线程完成后,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。

JS阻碍渲染

因为 JS 和渲染线程都在浏览器的渲染主线传上,执行 JS 时,渲染只能等待,直到 JS 渲染完毕。因此如果 JS 代码执行时间过长,会有阻碍渲染的麻烦。

任务优先级

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

根据 W3C 最新解释是这么说的:

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

在目前 chrome 的实现中,至少包含下面的队列:

  • 延时队列:用于存放计时器,优先级 中

  • 交互队列:用于存放用户交互后的函数,优先级 高

  • 微队列:用户存放需要最快执行的任务,优先级 最高

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

c
enum class TaskType: unsigned char{
  kUserInteraction = 2, // 用户交互
  kNetWorking = 3, // 网络
  kJavaScriptTimeout = 11, // 定时器
  kWebSocket = 12,
}

浏览器还有很多其他队列,但是和我们开发关系不大,不做考虑。

总结

浏览器的进程模型,描述浏览器在启动后,它会开启多个进程,原因是为了隔离,避免一个进程的功能运行崩溃了之后影响到其他进程。隔离之后,一个进程崩溃不会影响到别人。

其次重点讲了渲染进程,特别是渲染进程里边的主线程叫做渲染主线程,因为渲染主线程承担的事情太多了,因此,他找到了一种方式来解决事情混乱的问题,排队。

这些事情一件一件处理,不要同时处理几件,这样使用一个队列的方式,把一个任务一个任务取出来执行,执行完了之后再取下一个,这就是事件循环,实际上它本质上呢,就是用了一个无限的循环,每一次循环取一个任务执行,执行完了取下一个,下一次循环取下一个。

那么这个事件循环呢,其实跟异步就有关系,如果说我们使用同步的方式呢,就会造成阻塞,而用这种事件循环的方式,把任务加到排队,列边排队,就不会有这个阻塞的问题

  • 事件循环是异步的实现方式
  • 单线程是异步产生的原因

有了异步之后,线程永不阻塞,在事件循环里边实际上使用了多个队列,有微队列,有延时队列,有交互队列,最先执行的一定是微队列,每一次取任务的时候,优先从微队列里面去取,取完了之后再去看其他队列。

在延时队列和交互队列这一块呢,目前来讲是交互队列优先区,然后延时队列的后区,那将来会怎么样不知道,但一定是微队列首先区,它的优先级是最高的。