JavaScript事件循环详解:概念、原理与实践

发表时间: 2024-06-28 14:34

事件循环是许多开发人员感到困惑的根源并且也还是很多刚开始工作的程序员面试经常被问的问题,但它是 JavaScript 引擎的一个基本部分。它使 JavaScript 能够单线程运行,但能够以非阻塞方式执行。要理解事件循环,我们首先需要解释一些有关 JavaScript 引擎的内容,例如调用堆栈、任务、微任务及其各自的队列。下面小编谈谈我的理解。

调用堆栈

调用堆栈是一种跟踪 JavaScript 代码执行情况的数据结构(JavaScript 使用调用堆栈来管理执行上下文(execution context))。顾名思义,它是一个堆栈,是内存中的 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.调用堆栈现在为空。

任务和任务队列

任务是在JavaScript运行环境中被调度执行的代码块。在执行时,它们可以独占访问调用堆栈,也可以将其他任务加入队列。在任务之间,浏览器可以执行渲染更新。任务存储在任务队列中,等待由其相关函数执行。任务队列又是一种 FIFO(先进先出)数据结构。任务的示例包括与事件关联的事件侦听器的回调函数和的回调setTimeout()。

微任务和微任务队列

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

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

事件循环

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

1.脚本评估:同步执行脚本,直到调用堆栈为空。

2.任务处理:选择任务队列中的第一个任务并运行,直到调用堆栈为空。

3.微任务处理:选择微任务队列中的第一个微任务并运行,直到调用栈为空,重复直到微任务队列为空。

4.渲染:重新渲染 UI 并循环回到步骤 2。

举例:

console.log('Script start');setTimeout(() => console.log('setTimeout()'), 0);Promise.resolve()  .then(() => console.log('Promise.then() #1'))  .then(() => console.log('Promise.then() #2'));console.log('Script end');// console.log://   Script start//   Script end//   Promise.then() #1//   Promise.then() #2//   setTimeout()

是否和您想的一样?让我们一步一步分析一下发生了什么:

1.调用堆栈最初是空的。事件循环开始评估脚本。

2.console.log()被推送到调用堆栈并执行,记录'Script start'。

3.setTimeout()被推送到调用堆栈并执行。这会在任务队列中为其回调函数创建一个新任务。

4.Promise.prototype.resolve()被推送到调用堆栈并执行,依次调用Promise.prototype.then()。

5.Promise.prototype.then()被推送到调用栈并执行。这会在微任务队列中为其回调函数创建一个新的微任务。

6.console.log()被推送到调用堆栈并执行,记录'Script end'。

7.事件循环已完成其当前任务,并评估了脚本。然后它开始运行微任务队列中的第一个微任务,即Promise.prototype.then() 步骤 5 中排队的回调。

8.console.log()被推送到调用堆栈并执行,记录'Promise.then() #1'。

9.Promise.prototype.then()被推送到调用堆栈并执行。这会在微任务队列中为其回调函数创建一个新条目。

10.事件循环检查微任务队列。由于它不为空,因此它执行第一个微任务,即Promise.prototype.then()步骤 9 中排队的回调。

11.console.log()被推送到调用堆栈并执行,记录'Promise.then() #2'。

12.如果有的话,重新渲染就会在这里发生。

13.微任务队列为空,因此事件循环移至任务队列并执行第一个任务,即setTimeout()步骤 3 中排队的回调。

14.console.log()被推送到调用堆栈并执行,记录'setTimeout()'。

15.如果有的话,重新渲染就会在这里发生。

16.调用堆栈现在为空。

总结:

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

欢迎大家关注小编的公众号,小编每天都会分享国内外新知识哦!!!