基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

什么是浏览器的事件循环 (Event Loop)?

知识点图片

浏览器的事件循环 (Event Loop) 是 JavaScript 运行时环境(Runtime)处理同步和异步任务的核心机制。

简单来说,它是 JavaScript 实现“单线程非阻塞”的关键。它负责协调代码执行、事件处理(如点击)、定时器执行和网络请求回调等。

以下是通俗易懂的详细解释:


1. 为什么需要事件循环?

JavaScript 是单线程的。这意味着它同一时间只能做一件事。
如果 JS 只有同步执行(一行一行往下走),那么当遇到一个耗时的操作(比如从服务器获取数据)时,整个页面就会“卡死”,用户无法点击任何东西,直到数据回来。

为了解决这个问题,浏览器采用了异步机制。Event Loop 就是负责管理这些异步任务何时进入主线程执行的“调度员”。


2. 核心概念与组件

要理解 Event Loop,需要知道以下几个区域:

  1. 调用栈 (Call Stack)

    • 这是主线程,负责执行所有的同步代码
    • 代码执行时遵循“后进先出”原则。
    • 只有当调用栈为空时,Event Loop 才会去搬运异步任务进来。
  2. Web APIs (浏览器提供的能力)

    • 当 JS 遇到 setTimeoutfetchDOM事件 时,会把这些任务交给浏览器的 Web APIs 去处理(比如浏览器有专门的计时器线程、网络线程)。
    • 当这些任务完成(时间到了、数据回来了),它们会将回调函数放入任务队列
  3. 任务队列 (Task Queues)

    • 这是异步任务排队等待进入调用栈的地方。
    • 队列分为两种:宏任务队列微任务队列

3. 宏任务 (Macro-task) vs 微任务 (Micro-task)

这是面试中最常考的细节,两者的优先级不同。

  • 宏任务 (Macro-task)

    • 代表性的任务:script (整体代码)、setTimeoutsetIntervalsetImmediate (Node.js)、I/O、UI 渲染。
    • 特点:每次 Event Loop 循环只取一个宏任务执行。
  • 微任务 (Micro-task)

    • 代表性的任务:Promise.then/catch/finallyMutationObserverqueueMicrotask
    • 特点:优先级高。在当前宏任务执行完后,所有排队的微任务会一次性全部执行完,才会进行下一次循环。

4. Event Loop 的执行流程 (循环步骤)

浏览器的 Event Loop 遵循以下严格步骤:

  1. 执行栈选择:从宏任务队列中取出一个任务(刚开始是整个 script 代码)放入调用栈执行。
  2. 执行同步代码:执行过程中,如果遇到微任务(如 Promise),将其放入微任务队列;如果遇到宏任务(如 setTimeout),交给 Web API 处理,完成后放入宏任务队列。
  3. 清空微任务队列:当调用栈为空(当前宏任务执行完毕)时,立即查看微任务队列。如果有任务,就一个接一个地执行,直到微任务队列清空。
  4. UI 渲染:浏览器判断是否需要更新页面(通常是 60Hz,即每 16.6ms 一次)。如果需要,就在这里渲染。
  5. 下一轮循环:回到第 1 步,从宏任务队列中取出下一个任务执行。

简记口诀:

同步代码 -> 清空所有微任务 -> (尝试渲染) -> 取出一个宏任务 -> 循环


5. 实战代码示例

请看下面的代码,试着预测输出顺序:

javascript
console.log('1'); // 同步

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

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

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

执行分析:

  1. 第一轮 (Script 宏任务):

    • 遇到 console.log('1') -> 输出 1
    • 遇到 setTimeout -> 它是宏任务,交给 Web API 计时(0ms),回调函数进入宏任务队列等待。
    • 遇到 Promise.then -> 它是微任务,回调函数进入微任务队列等待。
    • 遇到 console.log('4') -> 输出 4
    • Script 代码执行完毕,调用栈清空。
  2. 检查微任务:

    • 发现微任务队列里有 console.log('3')
    • 执行它 -> 输出 3
    • 微任务队列清空。
  3. 开启下一轮 (新的宏任务):

    • 检查宏任务队列,发现 setTimeout 的回调。
    • 执行它 -> 输出 2

最终结果: 1 -> 4 -> 3 -> 2


6. 进阶示例 (嵌套情况)

javascript
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

分析:

  1. 同步:输出 1,输出 6
    • 宏队列:[setTimeout(log2)]
    • 微队列:[Promise(log4)]
  2. 清空微任务:执行 Promise(log4) -> 输出 4
    • 执行过程中遇到了 setTimeout(log5),将其加入宏队列。
    • 宏队列现在是:[setTimeout(log2), setTimeout(log5)]
  3. 第一轮结束
  4. 下一轮宏任务:取出 setTimeout(log2) 执行 -> 输出 2
    • 执行中遇到 Promise(log3),加入微队列。
  5. 清空微任务:执行 Promise(log3) -> 输出 3
  6. 下一轮宏任务:取出 setTimeout(log5) 执行 -> 输出 5

总结

  • JS 是单线程的,依靠 Event Loop 处理异步。
  • 同步代码最先执行。
  • 微任务 (Promise) 的优先级高于 宏任务 (setTimeout)。
  • 微任务会在当前宏任务结束、下一次渲染之前全部执行完。
00:00
00:00