Skip to content

JavaScript中的事件循环是什么?

事件循环对许多开发人员来说是一个令人困惑的问题,但它是JavaScript引擎的一个基本组成部分。它使JavaScript能够以非阻塞的方式在单线程中执行。要理解事件循环,我们首先需要解释一些关于JavaScript引擎的内容,例如调用栈、任务、微任务及其各自的队列。让我们逐个解释它们。

调用栈

调用栈是一种数据结构,用于跟踪JavaScript代码的执行。顾名思义,它是一个栈,因此是一种后进先出(LIFO)的内存数据结构。每个执行的函数在调用栈中表示为一个帧,并放置在前一个函数的顶部。

让我们逐步看一个简单的例子:

function foo() {
  console.log('foo');
  bar();
}

function bar() {
  console.log('bar');
}
  1. 调用栈最初为空。
  2. 函数foo()被推入调用栈。
  3. 函数foo()被执行并从调用栈中弹出。
  4. 函数console.log('foo')被推入调用栈。
  5. 函数console.log('foo')被执行并从调用栈中弹出。
  6. 函数bar()被推入调用栈。
  7. 函数bar()被执行并从调用栈中弹出。
  8. 函数console.log('bar')被推入调用栈。
  9. 函数console.log('bar')被执行并从调用栈中弹出。
  10. 调用栈现在为空。

任务和任务队列

任务是计划的同步代码块。在执行过程中,它们独占调用栈,并且还可以将其他任务入队。在任务之间,浏览器可以执行渲染更新。任务存储在任务队列中,等待由其关联的函数执行。任务队列本身是一种先进先出(FIFO)的数据结构。任务的示例包括与事件相关联的事件侦听器的回调函数和setTimeout()的回调函数。

微任务和微任务队列

微任务与任务类似,它们是被调度的同步代码块,在执行时独占访问调用栈。此外,它们存储在自己的FIFO(先进先出)数据结构中,即微任务队列。然而,微任务与任务的不同之处在于,在重新渲染之前,必须清空微任务队列。微任务的例子包括Promise回调和MutationObserver回调。

微任务和微任务队列也被称为作业和作业队列。

事件循环

最后,事件循环是一个持续运行的循环,检查调用栈是否为空。它通过将任务和微任务逐个放入调用栈中来处理它们,并控制渲染过程。它由四个关键步骤组成:

  1. 脚本评估:同步执行脚本,直到调用栈为空。
  2. 任务处理:选择任务队列中的第一个任务,并运行它,直到调用栈为空。
  3. 微任务处理:选择微任务队列中的第一个微任务,并运行它,直到调用栈为空,重复此过程直到微任务队列为空。
  4. 渲染:重新渲染UI,并返回到步骤2。

一个实际的例子

为了更好地理解事件循环,让我们看一个实际的例子,结合上述所有概念:

console.log('脚本开始');

setTimeout(() => console.log('setTimeout()'), 0);

Promise.resolve()
  .then(() => console.log('Promise.then() #1'))
  .then(() => console.log('Promise.then() #2'));

console.log('脚本结束');

// 输出:
//   脚本开始
//   脚本结束
//   Promise.then() #1
//   Promise.then() #2
//   setTimeout()

输出结果是否符合您的预期?让我们逐步分解一下发生的情况:

  1. 调用栈最初为空。事件循环开始评估脚本。
  2. console.log() 被推入调用栈并执行,打印 '脚本开始'
  3. setTimeout() 被推入调用栈并执行。这在任务队列中创建了一个新的任务,该任务的回调函数在任务队列中排队。
  4. Promise.prototype.resolve() 被推入调用栈并执行,依次调用 Promise.prototype.then()
  5. Promise.prototype.then() 被推入调用栈并执行。这在微任务队列中为其回调函数创建了一个新的微任务。
  6. console.log() 被推入调用栈并执行,打印 '脚本结束'
  7. 事件循环完成了当前任务的评估。然后开始运行微任务队列中的第一个微任务,即在步骤5中排队的 Promise.prototype.then() 的回调函数。
  8. console.log() 被推入调用栈并执行,打印 'Promise.then() #1'
  9. Promise.prototype.then() 被推入调用栈并执行。这在微任务队列中为其回调函数创建了一个新的条目。
  10. 事件循环检查微任务队列。由于不为空,它执行第一个微任务,即在步骤9中排队的 Promise.prototype.then() 的回调函数。
  11. console.log() 被推入调用栈并执行,打印 'Promise.then() #2'
  12. 如果有的话,此处会进行重新渲染。
  13. 微任务队列为空,所以事件循环转到任务队列并执行第一个任务,即在步骤3中排队的 setTimeout() 的回调函数。
  14. console.log() 被推入调用栈并执行,打印 'setTimeout()'
  15. 如果有的话,此处会进行重新渲染。
  16. 调用栈现在为空。

总结

  • 事件循环 负责执行 JavaScript 代码。它首先评估和执行脚本,然后处理 任务微任务
  • 任务微任务 是被调度的同步代码块。它们逐个执行,并分别放置在 任务队列微任务队列 中。
  • 对于所有这些,调用栈 用于跟踪函数调用。
  • 每当执行 微任务 时,必须先清空 微任务队列,然后才能执行下一个 任务
  • 渲染 发生在 任务 之间,但不会发生在 微任务 之间。

注意事项

  • 事件循环的脚本评估步骤本身类似于一个任务。
  • setTimeout() 的第二个参数表示执行的最小时间,而不是保证的时间。这是因为任务按顺序执行,并且微任务可能在其中执行。
  • Node.js 中事件循环的行为类似,但有一些差异。最显著的是,没有渲染步骤。
  • 较旧的浏览器版本不完全遵守操作顺序,因此任务和微任务可能以不同的顺序执行。