Bootstrap

一篇梳理清楚JavaScript 事件循环机制

JavaScript 是一种单线程语言,意味着它一次只能执行一段代码。然而,它的强大之处在于可以通过事件循环(Event Loop)实现异步操作,使得非阻塞的 I/O 成为可能。为了深入理解事件循环机制,我们需要了解以下几个概念:


1. 执行上下文(Execution Context)

执行上下文是 JavaScript 执行代码的环境,可以分为:

  • 全局执行上下文:默认存在,用于处理全局代码。
  • 函数执行上下文:每当调用函数时,都会创建一个新的执行上下文。
  • Eval 执行上下文:用于 eval() 代码。

2. 调用栈(Call Stack)

调用栈用于管理函数调用顺序。每当一个函数被调用时,会被压入栈中;函数执行完毕后,会被弹出。

示例代码:

function first() {
    console.log('First');
    second();
}
function second() {
    console.log('Second');
}
first();

执行流程:

  1. 调用 first(),压入栈;
  2. 执行 console.log('First'),然后调用 second()
  3. second() 被压入栈,执行 console.log('Second')
  4. second() 执行完毕,弹出栈;
  5. 最后 first() 执行完毕,弹出栈。

3. 异步任务和任务队列

JavaScript 的异步操作(如定时器、网络请求)不会阻塞主线程,而是将回调函数推送到任务队列中,等待调用栈空闲后由事件循环调度执行。

任务可以分为两类:

  1. 宏任务(Macro Task):包括 setTimeoutsetInterval、UI 渲染等。
  2. 微任务(Micro Task):包括 Promise.thenMutationObserver 等。

优先级:
微任务优先于宏任务。每当一个任务完成时,先检查是否有微任务队列,有的话执行所有微任务,然后才执行下一个宏任务。


4. 事件循环(Event Loop)

事件循环的核心作用是协调调用栈和任务队列。具体流程如下:

  1. 执行调用栈中的任务;
  2. 调用栈为空时,检查微任务队列并执行;
  3. 微任务队列为空后,从宏任务队列取一个任务执行;
  4. 重复以上过程。

代码示例与流程分析

以下代码展示了事件循环的工作机制:

console.log('Start');

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

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

console.log('End');

输出结果:

Start
End
Promise
Timeout

执行流程:

  1. console.log('Start') 立即执行,输出 Start
  2. setTimeout 的回调被推入宏任务队列;
  3. Promise.resolve().then 的回调被推入微任务队列;
  4. console.log('End') 立即执行,输出 End
  5. 执行微任务队列中的 Promise 回调,输出 Promise
  6. 执行宏任务队列中的 setTimeout 回调,输出 Timeout

5. 复杂示例分析

console.log('Script Start');

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

setTimeout(() => {
    console.log('SetTimeout 2');
    Promise.resolve().then(() => {
        console.log('Promise inside SetTimeout');
    });
}, 0);

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

console.log('Script End');

输出结果:

Script Start
Script End
First Promise
Second Promise
SetTimeout 1
SetTimeout 2
Promise inside SetTimeout

执行流程:

  1. 同步任务 console.log('Script Start')console.log('Script End') 立即执行;
  2. 第一个 setTimeout 和第二个 setTimeout 的回调推入宏任务队列;
  3. Promise.resolve() 的第一个 .then() 回调进入微任务队列;
  4. 微任务队列中的 First Promise 执行完成后,Second Promise 再次进入微任务队列并执行;
  5. 宏任务队列中,第一个 setTimeout 执行;
  6. 宏任务队列中,第二个 setTimeout 执行,同时它的内部 Promise.then() 回调进入微任务队列;
  7. 微任务队列中的 Promise inside SetTimeout 执行。

6. 如何测试事件循环顺序

可以通过如下代码进一步验证:

console.log('Global Start');

setTimeout(() => console.log('Macro Task'), 0);

Promise.resolve().then(() => console.log('Micro Task 1'))
                .then(() => console.log('Micro Task 2'));

console.log('Global End');

输出顺序:

Global Start
Global End
Micro Task 1
Micro Task 2
Macro Task

7. 最佳实践

  1. 尽量避免阻塞主线程:
    • 使用异步操作分解任务。
  2. 优先处理微任务:
    • 使用 Promise 等机制优化任务分配。
  3. 调试工具:
    • 使用浏览器开发者工具观察调用栈、任务队列等。

通过以上讲解和代码示例,相信你对 JavaScript 的事件循环机制有了更清晰的理解!

;