event loop.svg
为了协调事件、用户交互、脚本、渲染、网络等,用户代理必须使用本节所述的事件循环。每个代理都有一个关联的事件循环,该循环对于该代理是唯一的。

事件循环会驱动发生在浏览器中与用户交互有关的一切,共有三种:

window事件循环:
window事件循环驱动所有同源的窗口,这些窗口的事件同一由一个事件循环调度
在特定的情况下,同源窗口之间共享一个事件循环,特定的情况依赖于浏览器的具体实现,各个浏览器可能并不一样。同源窗口共享一个循环,比如:

  • 如果一个窗口打开了另一个窗口,它们可能会共享一个事件循环。
  • 如果窗口是包含在iframe中,则他可能会包含它的窗口共用一个
  • 在多进程浏览器中多个窗口碰见共享了同一个进程

worker事件循环:
worker事件循环是用来驱动worker的事件循环,包括基本的worker和shared worker或者service worker。 worker被放在一个或多个独立于“主代码”的代理中,浏览器可能会用单个或者多个事件循环来处理给定类型的所有worker

worklet事件循环:
worker事件循环时用于驱动运行workerlet的代理,包含worklet,audioWorklet, 和PaintWorklet

Javascript有一个基于事件循环的并发模型,事件循环主要负责执行代码,收集和处理事件以及执行队列中的子任务。同队列中的异步任务一次循环只能被执行一个。

同步的任务是指,只有前一个任务执行完毕才能执行后一个任务。异步任务是不直接进入主线程,而进入任务队列的任务。只有任务队列通知主线程,异步的任务需要执行时,该任务才会被放到主线程执行

由若干个函数调用形成了一个由若干帧组成的栈,这个就是程序执行时产生的执行栈。执行中遇到的对象等引用类型会被分配在堆中,JavaScript运行时包含了一个待处理消息的消息队列,一条消息会关联一个callback。 在事件循环期间的某一个时刻,运行时会从消息队列中弹出一个回调,放入执行栈中执行。

对于绑定的callback处理,会一直进行到执行栈再次为空为止,然后事件循环将会处理队列中下一个消息

  1. Promise.resolve().then(() => {
  2. console.log("promise 1");
  3. });
  4. function fn() {
  5. console.log("fn");
  6. Promise.resolve().then(() => {
  7. console.log("fn promise");
  8. });
  9. console.log("fn end");
  10. }
  11. fn();
  12. console.log("return fn");
  13. Promise.resolve().then(() => {
  14. console.log("promise 2");
  15. });
  16. // out
  17. fn
  18. fn end
  19. return fn
  20. promise 1
  21. fn promise // 并不会在函数中执行,任务当前的fn函数正在执行,执行栈不为空
  22. promise 2

每一个消息完整的执行后,其他消息才会被执行,所以一个函数的执行只能主动退出,不可能被抢占
这种机制的一个缺点就在于,当一个callbak执行时间过长,这段时间web应用程序就无法处理与用户的交互,例如点击事件、划拉。 所以应当缩小每一个消息处理事件,如果运行应将消息裁剪为多个消息,多次循环中执行。

微任务(micro)和宏任务(macro)

一个任务就是之计划由标准机制来执行的任何js,比如程序的初始化,事件触发的回调。除了添加事件还可以使用setTimeout() setInterval()来添加任务

每当一个任务存在,事件循环都会检查该任务是否正在把控制权交给其他js代码,否则,事件循环就会运行微任务队列中的所有微任务,接下来微任务循环在事件循环的每次迭代中被多次处理,包括处理完事件和其他回调以后

如果一个微任务向微任务队列中新加了一个或多个微任务,则那些新加入的微任务会早于下一个任务运行,因为事件循环会持续调用微任务直至队列为空,即使由更多微任务持续被加入的情况下

任务队列和微任务的区别

任务队列:
当执行来自任务队列中的任务时, 在每一次新的事件循环开始迭代的时候运行时都会从执行队列中的每一个任务,在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行

在以下时机中,新出现的任务会被添加到任务队列:

  • 一段新程序或者子程序被直接执行时
  • 触发了一个事件,将其回调函数添加到任务队列
  • 执行到一个由setTimeout() setInterval()创建的任务,回调函数会被添加到任务队列中

微任务:
每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会一次被执行。 不同的时它会等到微任务队列为空才会停止—-即使中途有微任务的新加入。

微任务可以添加新的微任务到队列中, 并在下一个任务开始执行之前且当前事件循环结束之前执行所有的微任务


当一些需要大量时间计算的代码,同步阻塞或者进入了无线循环,界面会卡死,这种问题可以使用以下方式解决:

  • 使用worker, 可以让主线程另起新的线程来运行脚本。

  • 通过使用promise异步的API可以使主线程在等待结果的同时继续执行其他任务。

微任务是另一种解决该问题的方法,通过将代码安排在下一次事件循环开始之前运行而不是必须要等待下一次开始之后执行

创建微任务的方式除了promise还有一个queueMicrotask() 函数,可以创建一个统一的微任务队列,它能够在任务时候,即便是当js执行上下文栈中没有执行上下文剩余时,也可以将代码安排在一个安全的时间运行。

Event loops

事件循环有一个或多个任务队列。 微任务队列不是队列

任务队列是集合,而不是队列,因为事件循环处理模型的第一步从所选队列中获取第一个可运行任务,而不是将第一个任务从队列中取出。

对于同一个事件源的task必须放在同一个任务队列中,不同来源的则被添加到不同的队列进行分组和序列化

每个事件循环都有一个当前正在运行的任务,该任务可以是任务或者null。 最初为空,用于处理重入

每个事件循环都有一个微任务队列,它是一个微任务队列,最初为空。微任务是一种口语化的方式,指的是通过队列和微任务算法创建的任务。

每个事件循环都有一个微任务队列,它是一个微任务队列,最初为空。微任务是一种口语化的方式,指的是通过队列和微任务算法创建的任务。

常见的任务:

Events

在特定EventTarget对象上调度事件对象通常由专用任务完成。

并非所有事件都使用任务队列进行调度;许多是在其他任务中分派的。

Parsing

HTML解析器标记一个或多个dom节点时,然后处理任何生成的标记

callback

调用回调通常由专用任务完成。

使用异步资源

当使用非阻塞方式获取资源时, 当部分资源可用时有任务来执行资源处理

Dom manipulation

对dom进行操作时,比如插入一个节点

只要事件环存在就必须执行以下步骤:

  1. 文档开始,让taskQueue成为事件循环的任务队列之一,一个任务任务队列至少包含一个可以运行的任务。 如果任务队列不存在,就开始执行微任务
  2. 让之前留下的oldestTask 陈各位itaskQueue中一个可运行的任务,并将其从taskQueue中删除
  3. 将事件循环的的当前执行任务设置为oldestTask
  4. 让taskStartTime为当前高分辨率时间。
  5. 执行oldestTask, 并重复以上步骤
  6. 将事件循环当前正在运行的任务设置回null.
  7. 微任务:执行微任务检查点。
  8. hasARenderingOpportunity 设置为false

    会用于计算didline

  9. 现在是当前的高分辨率时间。[HRT]

  10. 报告任务的执行时间
  11. 执行UI更新

    中间省略了UI、动画, 和worker中微任务的执行时机

  12. 当微任务队列不为空时:

    1. 让oldestMicrotask成为从事件循环的微任务队列中退出队列的结果。
    2. 将事件循环当前正在运行的任务设置为oldestMicrotask。
    3. 运行oldestMicrotask。
    4. 将事件循环当前正在运行的任务设置回null。
    5. 对于负责的事件循环是此事件循环的每个环境设置对象,通知该环境设置对象上已拒绝的承诺。
    6. 清除执行事务索引
    7. 将执行微任务检查点的事件循环设置为false。

requestAnimationFrame

event loop 中在样式重绘前被执行

requestIdleCallback

在事件循环闲置被执行, 一定在重绘之后, 但是和timeout会不确定顺序的执行, 在postMessag之后

image.png