一篇带你搞懂JavaScript Event Loop的小短文

153 阅读3分钟

在 JavaScript 中,Event Loop 负责协调执行栈、回调队列(宏任务队列)和微任务队列之间的调度。当执行栈为空时,Event Loop 会根据一定的顺序来调度微任务和宏任务。下面我将详细解释这个过程。

基础知识点回顾

  1. 执行栈、回调队列和微任务队列

    • 执行栈:一个后进先出(LIFO)的数据结构,用于储存当前正在执行的函数的执行上下文。
    • 回调队列(宏任务队列):一个先进先出(FIFO)的数据结构,用于储存待处理的宏任务回调函数。
    • 微任务队列:用于储存待处理的微任务回调函数。
  2. 微任务与宏任务

    • 宏任务 包括:script、setTimeout、setInterval、I/O、UI rendering 等。
    • 微任务 包括:Promise、MutationObserver、process.nextTick(Node.js)等。

调度流程

  1. 初始化:JavaScript 引擎启动时,会创建一个全局执行上下文,并将其压入执行栈。
  2. 执行同步代码:JavaScript 引擎执行同步代码,遇到异步操作时会注册回调函数,并将控制权交还给 Event Loop。
  3. 等待:Event Loop 持续检查执行栈是否为空,同时监听异步操作的完成。
  4. 宏任务执行:当执行栈为空且回调队列中有待处理的任务时,Event Loop 会从回调队列中取出第一个宏任务,并创建一个新的函数执行上下文,然后将其压入执行栈。
  5. 执行微任务:当当前宏任务执行完毕后,Event Loop 会检查是否有微任务需要执行。如果有,它会执行所有的微任务,直到微任务队列为空。
  6. 重复:此过程会不断重复,直到回调队列为空。

示例

  1. 我们先来看一个简单一点的示例:
console.log('Start'); 

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

Promise.resolve().then(() => { 
    console.log('Promise');
}); 

console.log('End');

// 输出结果为:Start  End  Promise  Timeout

这是因为 Promise.then 回调是微任务,而 setTimeout 的回调是宏任务。微任务会在当前宏任务执行完毕后立即执行,但在下一个宏任务开始之前。

  1. 我们来看另一个示例加深一下理解:
console.log(1);

setTimeout(() => console.log(2));

Promise.resolve().then(() => console.log(3));

Promise.resolve().then(() => setTimeout(() => console.log(4)));

Promise.resolve().then(() => console.log(5));

setTimeout(() => console.log(6));

console.log(7);

// 输出结果为:1 7 3 5 2 6 4

我们来分析一下:

  • 立即输出数字 1 和 7,因为简单的 console.log 调用没有使用任何队列。

  • 然后,主代码流程执行完成后,开始执行微任务队列。

    • 其中有命令行:console.log(3); setTimeout(...4); console.log(5)
    • 输出数字 3 和 5setTimeout(() => console.log(4)) 将 console.log(4) 调用添加到了宏任务队列的尾部。
    • 现在宏任务队列中有:console.log(2); console.log(6); console.log(4)
  • 当微任务队列为空后,开始执行宏任务队列。并输出 26 和 4

总结

  • 宏任务:当执行栈为空时,Event Loop 会检查回调队列(宏任务队列),如果有待处理的宏任务,它会从中取出第一个宏任务并执行。
  • 微任务:当当前宏任务执行完毕后,Event Loop 会检查是否有微任务需要执行,如果有,它会执行所有的微任务,直到微任务队列为空。