文章列表

事件循环(event loop)

Champ2026.01.03 23:04访问量0 次阅读
事件循环(event loop)
事件循环是什么?

事件循环是 JavaScript 实现异步编程的核心机制,它解决了单线程语言如何处理并发操作的问题。

一、为什么需要事件循环?

JavaScript 是单线程语言,意味着同一时间只能执行一个任务。如果没有事件循环:

  • 遇到网络请求、文件读取等耗时操作时,整个线程会被阻塞
  • 页面会卡死,无法响应用户交互

事件循环通过异步回调机制,让 JavaScript 可以在等待 I/O 操作时继续执行其他代码。

二、核心组成部分

┌───────────────────────────┐
│        Call Stack         │ ← 当前正在执行的函数(执行上下文栈)
└─────────────┬─────────────┘
              │
              │ 执行完毕
              ↓
┌───────────────────────────┐
│   Web APIs / Node APIs    │ ← 浏览器/Node提供的异步API(定时器、网络请求等)
└─────────────┬─────────────┘
              │
              │ 异步任务完成,将回调放入队列
              ↓
┌───────────────────────────┐
│    Task Queue (宏任务队列) │ ← 宏任务回调(setTimeout、setInterval、I/O等)
│Microtask Queue (微任务队列)│ ← 微任务回调(Promise.then、process.nextTick等)
└─────────────┬─────────────┘
              │
              │ 事件循环从队列中取出任务
              ↓
┌───────────────────────────┐
│        Event Loop         │ ← 持续检查队列,将任务推入调用栈执行
└───────────────────────────┘

三、执行顺序规则

  1. 同步代码优先执行,遇到异步任务交给 Web APIs 处理
  2. 微任务优先级高于宏任务
  3. 同一层级的微任务会全部执行完,才会执行下一个宏任务
  4. 每个宏任务执行后都会清空微任务队列
// 经典面试题
console.log('1'); // 同步

setTimeout(() => {
    console.log('2'); // 宏任务
    Promise.resolve().then(() => {
        console.log('3'); // 微任务
    });
}, 0);

Promise.resolve().then(() => {
    console.log('4'); // 微任务
    setTimeout(() => {
        console.log('5'); // 宏任务
    }, 0);
});

console.log('6'); // 同步
// 输出顺序:1, 6, 4, 2, 3, 5

/**第一步:执行同步代码
执行 console.log('1'),输出 1
遇到 setTimeout,将其回调函数放入 宏任务队列(等待执行)
遇到 Promise.resolve().then,将其回调函数放入 微任务队列
执行 console.log('6'),输出 6
此时输出:1, 6
第二步:同步代码执行完毕,检查微任务队列
微任务队列中有第一个Promise的回调,立即执行:
执行 console.log('4'),输出 4
遇到内部的 setTimeout,将其回调放入 宏任务队列
此时输出:1, 6, 4
第三步:微任务队列清空,执行下一个宏任务
从宏任务队列中取出第一个 setTimeout 的回调执行:
执行 console.log('2'),输出 2
遇到 Promise.resolve().then,将其回调放入 微任务队列
此时输出:1, 6, 4, 2
第四步:宏任务中的微任务立即执行
当前宏任务执行完毕,检查微任务队列:
执行 console.log('3'),输出 3
此时输出:1, 6, 4, 2, 3
第五步:执行下一个宏任务
从宏任务队列中取出第二个 setTimeout 的回调执行:
执行 console.log('5'),输出 5
最终输出:1, 6, 4, 2, 3, 5
**/

四、任务分类详细

宏任务(Macrotasks/Tasks)

  • setTimeoutsetInterval
  • setImmediate(Node.js)
  • requestAnimationFrame(浏览器)
  • I/O 操作(文件读取、网络请求)
  • UI 渲染(浏览器)
  • 整体 script 代码

微任务(Microtasks)

  • Promise.then()Promise.catch()Promise.finally()
  • async/await(本质是 Promise)
  • MutationObserver(浏览器)
  • process.nextTick(Node.js,优先级最高)
  • queueMicrotask()
//示例
setTimeout(() => {
  console.log('Timeout callback');
}, 0);

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

console.log('Main thread execution');

//执行结果
Main thread execution
Promise callback
Timeout callback

五、async/await 的特殊处理

重点:await后面的代码相当于then回调(属于微任务)!

async function async1() {
    console.log('async1 start'); // 同步
    await async2(); // await后面的代码相当于then回调(微任务)
    console.log('async1 end'); // 微任务
}

async function async2() {
    console.log('async2'); // 同步
}

console.log('script start'); // 同步

setTimeout(() => {
    console.log('setTimeout'); // 宏任务
}, 0);

async1();

new Promise(resolve => {
    console.log('promise1'); // 同步
    resolve();
}).then(() => {
    console.log('promise2'); // 微任务
});

console.log('script end'); // 同步

//宏:settimeout,
//微:async1 end,promise2,

// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

七、面试常见问题与解答

Q1: setTimeout(fn, 0) 真的是0毫秒后执行吗?

不是。0毫秒表示尽快执行,但实际至少要等:

  • 当前执行栈清空
  • 微任务队列清空
  • 浏览器可能有最小延迟(通常4ms)

Q2: 如何实现一个0延迟的定时器?

// 使用 MessageChannel 或 postMessage
function zeroTimeout(fn) {
    const channel = new MessageChannel();
    channel.port1.onmessage = fn;
    channel.port2.postMessage(null);
}

Q3: 什么是"任务饥饿"?

如果微任务中不断产生新的微任务,会导致宏任务永远无法执行。

历史留言 (0)
ICP备案号浙ICP备2026065730号-1公安备案号浙公网安备33019202003213号