事件循环
事件循环是 JavaScript 引擎中一个至关重要的部分,它使得 JavaScript 能够在单线程中以非阻塞的方式执行。要理解事件循环是如何工作的,我们需要首先了解一些关键概念,包括调用堆栈、任务、微任务及它们各自所在的队列。
调用堆栈(Call Stack)
调用堆栈是一种数据结构,用于跟踪正在执行的 JavaScript 代码中的函数调用。顾名思义,调用堆栈是一个堆栈,即内存中的后进先出(LIFO)数据结构。每个被执行的函数都表示为调用堆栈中的一个帧,会在前一个函数之上入栈。
函数
foo()
被压入调用堆栈。函数
foo()
被执行并从调用堆栈中弹出。函数
console.log('foo')
被推送到调用堆栈上。函数
console.log('foo')
被执行并从调用堆栈中弹出。函数
bar()
被压入调用堆栈。函数
bar()
被执行并从调用堆栈中弹出。函数
console.log('bar')
被推送到调用堆栈上。函数
console.log('bar')
被执行并从调用堆栈中弹出。调用堆栈现在为空。
任务(Task)与任务队列(Task Queue)
任务是预定的、同步执行的代码块。在执行过程中,它们对调用堆栈具有独占访问权限,并且可以将其他任务排入队列。任务之间可执行渲染更新。任务存储在任务队列中,并等待着相关函数执行。任务队列是一个先进先出(FIFO)的数据结构。典型的任务示例包括事件监听器回调函数以及 setTimeout() 的回调。
微任务(Microtask)与微任务队列(Microtask Queue)
微任务与任务类似,因为它们也是预定的同步执行的代码块,并在执行过程中对调用堆栈具有独占访问权限。此外,它们还存储在自己的先进先出(FIFO)数据结构中,即微任务队列。然而,微任务与任务的区别在于,它们会在当前任务执行完毕之后以及重新渲染之前立即执行。典型的微任务示例包括 Promise 回调和 MutationObserver 回调。
事件循环(Event Loop)
事件循环是一个持续运行并检查调用堆栈是否为空的过程。它通过将任务和微任务逐个放入调用堆栈中来处理它们,并对渲染过程进行控制。事件循环的核心包含四个关键步骤:
脚本评估:逐个执行脚本内的代码,直到调用堆栈为空。
任务处理:选择任务队列中的第一个任务,然后执行它,一直执行到调用堆栈为空。
微任务处理:从微任务队列中取出第一个微任务执行,直至调用堆栈为空,重复此过程直至微任务队列为空。
渲染:重新渲染 UI 界面,并循环回到步骤 2。
实际示例
让我们逐步分析这个过程:
调用堆栈最初为空。事件循环开始评估脚本。
console.log()
入栈并执行,输出 "Script start"。setTimeout()
入栈并执行。该操作会在任务队列中为其回调函数创建一个新任务。Promise.prototype.resolve()
入栈并执行,然后依次调用Promise.prototype.then()
。Promise.prototype.then()
入栈并执行。这会在微任务队列中为其回调函数创建一个新的微任务。console.log()
入栈并执行,输出 "Script end"。事件循环已完成其当前任务(评估脚本),接着开始处理微任务队列中的第一个微任务,即在步骤 5 中排入队列的
Promise.prototype.then()
的回调。console.log()
入栈并执行,输出 "Promise.then() #1"。Promise.prototype.then()
入栈并执行。这会在微任务队列中为其回调函数创建一个新条目。事件循环检查微任务队列。因为它还不为空,所以会继续执行队列中的第一个微任务——在步骤 10 中排入队列的
Promise.prototype.then()
的回调。console.log()
入栈并执行,输出 "Promise.then() #2"。如果有的话,在此处进行重新渲染。
由于微任务队列已经为空,事件循环将转到任务队列,并执行第一个任务。这是在步骤 3 中排入队列的
setTimeout()
的回调。console.log()
入栈并执行,输出 "setTimeout()"。如果有的话,在此处进行重新渲染。
调用堆栈现在为空。
总结
事件循环负责执行 JavaScript 代码。首先处理脚本评估和执行,然后处理任务和微任务。
任务和微任务都是预定的同步代码块。它们逐个执行,并分别放置在任务队列和微任务队列中。
调用堆栈跟踪 JavaScript 中的函数调用。
每次执行微任务时,都需要清空微任务队列,然后才能进行下一个任务的处理。
渲染工作发生在任务之间,但不会发生在微任务之间。
附加说明
事件循环的脚本评估步骤本身与任务类似,即按顺序执行直至结束。
setTimeout() 的第二个参数表示执行前的最短延迟时间,而非保证时间。这是因为任务会按顺序执行,而微任务可能在此期间插入执行。
在 Node.js 中,事件循环的行为类似于在浏览器中的行为。但它们之间也存在一些差异,最明显的区别是不存在渲染步骤。
较旧版本的浏览器可能不会完全遵循这种操作顺序,因此任务和微任务可能会以不同的顺序执行。