面试官:讲一下输出顺序🙃

830 阅读3分钟

一位学长面试的时候遇到的题目

JavaScript 的事件循环(event loop)是一种机制,它负责执行异步代码并管理调用栈任务队列之间的交互,确保异步操作如定时器、 promises 和 I/O 操作能够在适当的时候被执行。简而言之,事件循环使得 JavaScript 能够处理并发操作,而无需多线程。

一、核心机制

  • 同步优先原则:主线程同步代码立即执行
  • 微任务优先原则:所有微任务(Promise、queueMicrotask)会在当前宏任务结束后立即执行
  • 渲染时机:每完成一个宏任务后可能触发页面渲染
  • 任务队列管理:宏任务队列(setTimeout等)每次只取一个任务执行

屏幕截图 2025-03-08 170438.png

二、异步任务类型

宏任务(MacroTask)

类型触发场景执行优先级所属环境关键特性
script 整体代码页面加载/脚本执行最高浏览器首个默认执行的宏任务
setTimeout定时器到期通用最小延迟4ms(浏览器规范)
setInterval周期性定时器通用可能堆积任务
I/O 操作文件读写、网络请求(fetch/XHR)通用回调进入任务队列
DOM 事件click/keydown 等交互事件浏览器事件触发时入队
requestAnimationFrame动画帧回调中高浏览器在渲染前执行
MessageChannel跨文档通信浏览器优先级高于 setTimeout
setImmediateNode.js 立即执行Node.js比 setTimeout(0) 更快
  • 宏任务执行特点: 每个宏任务执行后都会重新检查微任务队列
// 浏览器环境示例
setTimeout(() => console.log('Timeout1'), 0);
setTimeout(() => {
  console.log('Timeout2');
  setTimeout(() => console.log('Nested Timeout'), 0);
}, 0);

// 输出顺序:Timeout1 → Timeout2 → Nested Timeout

微任务(MicroTask)

类型触发机制执行优先级所属环境关键特性
Promise.thenPromise 状态变更(resolve/reject)通用链式调用形成微任务队列
queueMicrotask直接调用通用官方推荐的微任务API
MutationObserverDOM 变更观测浏览器替代已废弃的 Mutation Events
process.nextTickNode.js 阶段切换极高Node.js优先于所有微任务
  • 微任务执行特点: 微任务队列必须完全清空才会执行下一个宏任务
// 微任务嵌套示例
Promise.resolve().then(() => {
  console.log('Microtask1');
  queueMicrotask(() => console.log('Nested Microtask'));
});

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

/* 执行顺序:
Microtask1 → Microtask2 → Nested Microtask
 */

三、原理实践

console.log('同步任务开始');

setTimeout(() => {
  console.log('宏任务1启动');
  Promise.resolve().then(() => {
    console.log('宏任务1的微任务');
    queueMicrotask(() => console.log('嵌套微任务'));
  });
});

setTimeout(() => console.log('宏任务2'));

Promise.resolve().then(() => console.log('初始微任务'));

console.log('同步任务结束');

/* 执行结果:
同步任务开始
同步任务结束
初始微任务        ← 第一个宏任务(script)后的微任务
宏任务1启动
宏任务1的微任务   ← 第二个宏任务后的第一层微任务
嵌套微任务        ← 第二层嵌套微任务
宏任务2          ← 第三个宏任务 
*/

四、面试题解析

async function async1() {
  console.log('E'); // 1️⃣ 同步执行
  await async2();   // 2️⃣ 触发微任务
  console.log('F'); // 4️⃣ 微任务执行
}

async function async2() {
  console.log('G'); // 3️⃣ 同步执行
}

setTimeout(() => console.log('H'), 0); // 6️⃣ 宏任务

async1(); // 触发执行流

new Promise((res) => {
  console.log('I'); // 5️⃣ 同步执行
  res();
}).then(() => console.log('J')); // 5️⃣ 微任务

执行顺序详解:

  1. 同步阶段:

    • 执行 async1() 输出 ​E
    • 执行 async2() 输出 ​G
    • 执行 Promise 构造函数输出 ​I
  2. 微任务队列:

    • await async2() 后的代码 → 输出 ​F注意
    • Promise.then → 输出 ​J
  3. 宏任务队列:

    • setTimeout 回调 → 输出 ​H

关键机制解析:

// async/await 本质是 Promise 语法糖
async function example() {
  await somePromise;
  console.log('后续代码');
}

// 等价于
function example() {
  return somePromise.then(() => {
    console.log('后续代码');
  });
}

五、总结

希望下次我遇到这种题目不会陷入死一样的沉默😅