1.事件循环揭秘:宏任务与微任务

查看飞书原文 ↗2,901 字符
1. 核心概念​
​
为了在单线程模型中处理耗时的异步操作,JavaScript 引入了事件循环机制。理解事件循环的关键在于掌握以下几个概念:​
​
​
​
概念​
​
​
描述​
​
​
调用栈 (Call Stack)​
​
​
一个后进先出 (LIFO) 的数据结构,用于存储和管理函数调用。当执行一个函数时,它被推入栈顶;当函数返回时,它被弹出。所有同步代码都在调用栈中执行。​
​
​
任务队列 (Task Queue)​
​
​
一个先进先出 (FIFO) 的数据结构,用于存放待处理的异步任务的回调函数。它分为两种类型:宏任务队列和微任务队列。​
​
​
事件循环 (Event Loop)​
​
​
一个持续运行的进程,它不断地检查调用栈是否为空。如果为空,它会去检查微任务队列,然后是宏任务队列,并将队列中的任务回调函数推入调用栈中执行。​
​
​
2. 宏任务 (Macrotask) 与微任务 (Microtask)​
这是事件循环中最核心的区别,直接决定了代码的执行顺序。​
•
宏任务 (Macrotask):​
◦
描述: 通常是较大、独立的任务块。每次事件循环只会从宏任务队列中取出一个任务来执行。​
◦
常见例子:​
▪
setTimeout()​
▪
setInterval()​
▪
setImmediate() (Node.js 环境)​
▪
I/O 操作 (如文件读写、网络请求)​
▪
UI 渲染 (浏览器环境)​
​
•
微任务 (Microtask):​
◦
描述: 通常是需要尽快执行的、较小的任务,用于在当前任务之后、下一次宏任务之前完成一些更新或清理工作。​
◦
常见例子:​
▪
Promise.then(), Promise.catch(), Promise.finally()​
▪
async/await (其 await 之后的部分)​
▪
queueMicrotask()​
▪
MutationObserver​
​
3. 执行顺序​
事件循环的执行流程可以概括为以下步骤,并不断重复:​
1.
执行同步代码: 首先,将整个 script 脚本作为第一个宏任务,在调用栈中从头到尾执行所有同步代码。​
2.
清空微任务队列: 执行完同步代码后,检查微任务队列。如果队列不为空,则**一次性执行所有**的微任务,直到队列变空。​
◦
注意: 如果在执行微任务的过程中又产生了新的微任务,那么这些新的微任务也会被添加到队列的末尾,并在此轮中一并执行。​
3.
执行一个宏任务: 微任务队列清空后,从宏任务队列中取**一个**任务,推入调用栈中执行。​
4.
重复: 重复步骤 2 和 3。​
​
一句话总结:一次事件循环 = 执行完一个宏任务 -> 执行完所有微任务。​
​
​
​
4. 经典面试题解析​
让我们通过一个包含 setTimeout, Promise, 和 async/await 的经典例子来追踪代码的执行顺序。​
代码示例:​
​
async function async1() {
  console.log('2. async1 start');
  await async2();
  console.log('6. async1 end');
}

async function async2() {
  console.log('3. async2');
}

console.log('1. script start');

setTimeout(function() {
  console.log('8. setTimeout');
}, 0);

async1();

new Promise(function(resolve) {
  console.log('4. promise1');
  resolve();
}).then(function() {
  console.log('7. promise2');
});

console.log('5. script end');
​
逐步追踪执行流程:​
1.
同步代码执行:​
◦
console.log('1. script start') 执行。 输出: 1. script start​
◦
遇到 setTimeout,将其回调函数放入 宏任务队列。​
◦
调用 async1()。​
◦
console.log('2. async1 start') 执行。 输出: 2. async1 start​
◦
遇到 await async2():​
▪
async2() 函数立即执行,console.log('3. async2') 执行。 输出: 3. async2​
▪
await 关键字会暂停 async1 函数的执行,并将 await 后面的代码 (console.log('6. async1 end')) 放入 **微任务队列**。​
◦
遇到 new Promise:​
▪
Promise 的构造函数是同步执行的,console.log('4. promise1') 执行。 输出: 4. promise1​
▪
resolve() 被调用,Promise 状态变为 resolved,其 .then 的回调函数被放入 **微任务队列**。​
◦
console.log('5. script end') 执行。 **输出: 5. script end​
​
此时,同步代码已全部执行完毕。​
◦
调用栈: 空​
◦
宏任务队列: [setTimeout 回调]​
◦
微任务队列: [async1 end 回调, promise2 回调]​
​
2.
清空微任务队列:​
◦
事件循环发现调用栈为空,开始处理微任务队列。​
◦
取出第一个微任务 (async1 end 回调) 执行,console.log('6. async1 end')。 **输出: 6. async1 end​
◦
取出第二个微任务 (promise2 回调) 执行,console.log('7. promise2')。 **输出: 7. promise2​
​
此时,微任务队列已清空。​
​
3.
执行一个宏任务:​
◦
事件循环从宏任务队列中取出 setTimeout 的回调函数来执行。​
◦
console.log('8. setTimeout') 执行。 **输出: 8. setTimeout​
​
最终输出顺序:​
1. script start
2. async1 start
3. async2
4. promise1
5. script end
6. async1 end
7. promise2
8. setTimeout
​
通过这个例子,可以清晰地看到同步代码、async/await、Promise 和 setTimeout 之间复杂的交互关系,以及微任务优先于宏任务执行的核心原则。​
​