在 JavaScript 中,Event Loop 负责协调执行栈、回调队列(宏任务队列)和微任务队列之间的调度。当执行栈为空时,Event Loop 会根据一定的顺序来调度微任务和宏任务。下面我将详细解释这个过程。
基础知识点回顾
-
执行栈、回调队列和微任务队列
- 执行栈:一个后进先出(LIFO)的数据结构,用于储存当前正在执行的函数的执行上下文。
- 回调队列(宏任务队列):一个先进先出(FIFO)的数据结构,用于储存待处理的宏任务回调函数。
- 微任务队列:用于储存待处理的微任务回调函数。
-
微任务与宏任务
- 宏任务 包括:script、setTimeout、setInterval、I/O、UI rendering 等。
- 微任务 包括:Promise、MutationObserver、process.nextTick(Node.js)等。
调度流程
- 初始化:JavaScript 引擎启动时,会创建一个全局执行上下文,并将其压入执行栈。
- 执行同步代码:JavaScript 引擎执行同步代码,遇到异步操作时会注册回调函数,并将控制权交还给 Event Loop。
- 等待:Event Loop 持续检查执行栈是否为空,同时监听异步操作的完成。
- 宏任务执行:当执行栈为空且回调队列中有待处理的任务时,Event Loop 会从回调队列中取出第一个宏任务,并创建一个新的函数执行上下文,然后将其压入执行栈。
- 执行微任务:当当前宏任务执行完毕后,Event Loop 会检查是否有微任务需要执行。如果有,它会执行所有的微任务,直到微任务队列为空。
- 重复:此过程会不断重复,直到回调队列为空。
示例
- 我们先来看一个简单一点的示例:
console.log('Start');
setTimeout(() => { console.log('Timeout'); }, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
// 输出结果为:Start End Promise Timeout
这是因为 Promise 的 .then 回调是微任务,而 setTimeout 的回调是宏任务。微任务会在当前宏任务执行完毕后立即执行,但在下一个宏任务开始之前。
- 我们来看另一个示例加深一下理解:
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和5,setTimeout(() => console.log(4))将console.log(4)调用添加到了宏任务队列的尾部。 - 现在宏任务队列中有:
console.log(2); console.log(6); console.log(4)。
- 其中有命令行:
-
当微任务队列为空后,开始执行宏任务队列。并输出
2、6和4。
总结
- 宏任务:当执行栈为空时,Event Loop 会检查回调队列(宏任务队列),如果有待处理的宏任务,它会从中取出第一个宏任务并执行。
- 微任务:当当前宏任务执行完毕后,Event Loop 会检查是否有微任务需要执行,如果有,它会执行所有的微任务,直到微任务队列为空。