JavaScript 事件循环(Event Loop)机制详解

1. 基本概念

JavaScript 是单线程的语言,但通过事件循环(Event Loop)机制实现了非阻塞的异步执行。事件循环主要包含以下几个关键部分:

  1. 调用栈(Call Stack)
  2. 任务队列(Task Queue)/宏任务队列(Macrotask Queue)
  3. 微任务队列(Microtask Queue)
  4. Web APIs(浏览器环境)或 C++ APIs(Node.js环境)

2. 执行顺序

JavaScript 代码的执行顺序如下:

  1. 同步代码(在调用栈中直接执行)
  2. 微任务(microtask):Promise、process.nextTick、MutationObserver等
  3. 宏任务(macrotask):setTimeout、setInterval、setImmediate、I/O、UI渲染等

2.1 重要原则:

  • 每个宏任务执行完后,都会检查微任务队列
  • 如果微任务队列不为空,会先清空微任务队列
  • 只有等微任务队列清空后,才会执行下一个宏任务

在这里插入图片描述

3. 详细示例

3.1 基础示例

console.log('1'); // 同步代码

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

Promise.resolve().then(() => {
    console.log('3'); // 微任务
});

console.log('4'); // 同步代码

// 输出顺序:1 4 3 2

执行过程解析:

  1. 同步代码 console.log('1') 直接执行
  2. setTimeout 回调被放入宏任务队列
  3. Promise.then 回调被放入微任务队列
  4. 同步代码 console.log('4') 直接执行
  5. 调用栈清空后,执行微任务队列中的任务,输出 ‘3’
  6. 最后执行宏任务队列中的任务,输出 ‘2’

3.2 复杂示例

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

setTimeout(() => {
    console.log('setTimeout 1'); // 5
    Promise.resolve().then(() => {
        console.log('promise in setTimeout'); // 6
    });
}, 0);

Promise.resolve().then(() => {
    console.log('promise 1'); // 3
    setTimeout(() => {
        console.log('setTimeout 2'); // 7
    }, 0);
}).then(() => {
    console.log('promise 2'); // 4
});

console.log('script end'); // 2

// 输出顺序:
// script start
// script end
// promise 1
// promise 2
// setTimeout 1
// promise in setTimeout
// setTimeout 2

执行过程详细解析:

  1. 同步代码执行阶段

    • 执行 console.log('script start')
    • 遇到第一个 setTimeout,将其回调函数放入宏任务队列
    • 遇到 Promise.resolve().then(),将第一个 then 回调放入微任务队列
    • 执行 console.log('script end')
  2. 第一轮微任务执行阶段

    • 执行第一个 then 回调,输出 promise 1
    • 遇到第二个 setTimeout,将其回调放入宏任务队列
    • 第一个 then 执行完成会链式调用第二个 then,将其放入微任务队列
    • 继续执行微任务队列,输出 promise 2
  3. 第一个宏任务执行阶段

    • 执行第一个 setTimeout 的回调,输出 setTimeout 1
    • 遇到 Promise.resolve().then(),将其回调放入微任务队列
    • 该宏任务执行完毕,检查微任务队列
    • 执行微任务,输出 promise in setTimeout
  4. 第二个宏任务执行阶段

    • 执行第二个 setTimeout 的回调,输出 setTimeout 2

关键点说明:

  • Promise 链式调用的 then 会依次进入微任务队列
  • 每个宏任务执行完后,都会检查并清空微任务队列
  • setTimeout 虽然延时为 0,但会在当前同步代码和微任务都执行完后才执行

3.3 async/await 示例

async function async1() {
    console.log('async1 start'); // 2
    await async2();
    console.log('async1 end'); // 6
}

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

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

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

async1();

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

console.log('script end'); // 5

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

执行过程详细解析:

  1. 同步代码执行阶段

    • 执行 console.log('script start')
    • 遇到 setTimeout,将其回调放入宏任务队列
    • 遇到 async1() 调用:
      • 进入 async1 函数,执行 console.log('async1 start')
      • 遇到 await async2(),立即执行 async2 函数
      • 执行 async2 中的 console.log('async2')
      • await 会将后面的代码(console.log('async1 end'))放入微任务队列
    • 执行 new Promise 中的同步代码 console.log('promise1')
    • Promise.resolve() 将 then 的回调放入微任务队列
    • 执行 console.log('script end')
  2. 微任务执行阶段

    • 执行 await 后面的代码,输出 async1 end
    • 执行 Promise.then 的回调,输出 promise2
  3. 宏任务执行阶段

    • 执行 setTimeout 的回调,输出 setTimeout

关键点说明:

  • async 函数在执行时,遇到 await 会立即执行 await 后面的表达式
  • await 后面的代码会被转换成 Promise.then 的回调,放入微任务队列
  • async/await 本质上是 Promise 的语法糖,所以它的执行顺序遵循 Promise 的规则
  • async 函数内部的同步代码会立即执行,而不会进入任务队列

4. Node.js 事件循环的特点

Node.js 的事件循环比浏览器更复杂,包含六个阶段:

  1. timers:执行 setTimeout 和 setInterval 的回调
  2. pending callbacks:执行系统操作的回调
  3. idle, prepare:仅系统内部使用
  4. poll:检索新的 I/O 事件,执行 I/O 相关的回调
  5. check:执行 setImmediate 的回调
  6. close callbacks:执行 close 事件的回调
// Node.js 环境示例
const fs = require('fs');

console.log('start');

setImmediate(() => {
    console.log('setImmediate');
});

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

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

process.nextTick(() => {
    console.log('nextTick');
});

fs.readFile(__filename, () => {
    console.log('readFile');
});

console.log('end');

// 输出顺序:
// start
// end
// nextTick
// promise
// setTimeout
// setImmediate
// readFile

5. 面试题实践

5.1 示例一:setTimeout与Promise的执行顺序

console.log('1');

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

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

console.log('4');

解析
同步任务console.log('1')console.log('4')最先执行。
Promise的微任务.then()优先级高于setTimeout的宏任务,因此3先于2输出。
最终输出顺序:1, 4, 3, 2


5.2 示例二:async/await与定时器混合场景

async function asyncFunc() {
  console.log('A');
  await Promise.resolve().then(() => console.log('B'));
  console.log('C');
}

setTimeout(() => console.log('D'), 0);
asyncFunc();
Promise.resolve().then(() => console.log('E'));
console.log('F');

解析
同步代码按顺序执行A, F
await后的代码console.log('C')会被推入微任务队列,与E同属微任务。
微任务执行顺序按入队顺序:B, E, C
宏任务D最后执行。
最终输出顺序:A, F, B, E, C, D


6. 总结

  1. 执行顺序:同步代码 > 微任务 > 宏任务。
  2. 微任务(Promiseprocess.nextTick)先于宏任务(setTimeoutsetInterval)执行。
  3. await会暂停函数执行,后续代码变为微任务。
  4. 每个宏任务执行完后,都会检查微任务队列。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值