Event Loop详解

浏览器Event Loop执行流程

事件循环其实就是入栈出栈的循环。上面例子中说到了setTimeout,那setInterval呢,Promise呢等等等等,有很多异步的函数。但是这些异步任务有分宏任务(macro-task)和微任务(micro-task): macro-task包括: setTimeout, setInterval, setImmediate, I/O, UI rendering。 micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver。 每一次Event Loop触发时:

  1. 执行完主执行线程中的任务。
  2. 取出micro-task中任务执行直到清空。
  3. 取出macro-task中一个任务执行。
  4. 取出micro-task中任务执行直到清空。
  5. 重复3和4。

Node Event Loop执行流程

js执行为单线程,所有代码皆在主线程调用栈完成执行。当主线程任务清空后才会去轮询取任务队列中任务。

循环阶段

在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:

  1. timers:执行满足条件的setTimeout、setInterval回调。
  2. I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。
  3. idle,prepare:可忽略
  4. poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
  5. check:执行setImmediate的回调。
  6. close callbacks:关闭所有的closing handles,一些onclose事件。

几个队列

除上述循环阶段中的任务类型,我们还剩下浏览器和node共有的microtask和node独有的process.nextTick,我们称之为Microtask Queue和NextTick Queue。

我们把循环中的几个阶段的执行队列也分别称为Timers Queue、I/O Queue、Check Queue、Close Queue。

循环之前

在进入第一次循环之前,会先进行如下操作:

  • 同步任务
  • 发出异步请求
  • 规划定时器生效的时间
  • 执行process.nextTick()

开始循环

按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:

  • 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
  • 进入下轮循环。

可以看出,nextTick优先级比promise等microtask高。setTimeoutsetInterval优先级比setImmediate高。

注意

  • 如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。
  • setTimeout优先级比setImmediate高,但是由于setTimeout(fn,0)的真正延迟不可能完全为0秒,可能出现先创建的setTimeout(fn,0)而比setImmediate的回调后执行的情况。

伪代码



while (true) {
  loop.forEach((阶段) => {
    阶段全部任务()
    nextTick全部任务()
    microTask全部任务()
  })
  loop = loop.next
}

实际例子


setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);
复制代码结果是:3 4 6 8 7 5 2 1

优先级关系如下:

process.nextTick > promise.then > setTimeout > setImmediate

V8实现中,两个队列各包含不同的任务:

macrotasks: script(整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserver

参考

  1. https://juejin.im/post/5aa5dcabf265da239c7afe1e

原文发布时间为:2018年07月01日
原文作者: Jay_Gao
本文来源:  掘金  如需转载请联系原作者


<think>我们正在讨论JavaScript中的事件循环(Event Loop)。根据用户的问题和提供的引用,用户希望了解Event Loop的概念和工作机制,特别是在编程或系统执行上下文中的作用。 引用[1]提到:Event Loop是JS实现异步的原理,在浏览器和Node中有所不同,且对理解JS执行至关重要。 引用[2]提到:事件循环是处理和调度异步任务的核心机制,通过区分宏任务和微任务来确保执行顺序和优先级。 引用[3]提到:异步代码完成后需要等待程序空闲,事件循环就是处理同步和异步操作的机制。 引用[4]提到:在介绍Event Loop之前需要了解执行上下文、调用栈等概念,并给出了一个调用栈的例子。 因此,我们需要从以下几个方面展开: 1. Event Loop是什么(定义)? 2. 为什么需要Event Loop(解决的问题)? 3. Event Loop的工作机制(包括调用栈、任务队列、微任务队列等)? 4. 宏任务和微任务的区别? 5. 浏览器和Node.js中Event Loop的差异(简要提及)? 注意:由于用户问题聚焦在概念和工作机制,我们应重点解释工作机制,但不过度深入浏览器和Node.js的差异(除非必要)。 结构安排: 一、Event Loop的定义与背景 二、核心概念:调用栈、任务队列(宏任务和微任务) 三、Event Loop的工作流程(步骤) 四、示例分析 五、浏览器与Node.js的Event Loop差异(简要) 六、总结 同时,按照要求: 1. 使用中文回答。 2. 行内公式用$...$,独立公式用$$...$$(但本主题可能较少涉及数学公式,若有则用)。 3. 在回答末尾生成相关问题。 4. 引用文献在回答中自然标注。 由于引用中已经提供了部分基础,我们可以整合这些信息。 开始回答:</think>### Event Loop(事件循环)概念与工作机制详解 #### 一、Event Loop 的定义与核心作用 **Event Loop(事件循环)** 是 JavaScript 处理异步操作的核心机制,它通过**持续监控任务队列**协调同步代码与异步代码的执行顺序,解决单线程环境下的非阻塞 I/O 问题[^1][^3]。其核心作用包括: - **管理异步任务**:处理 `setTimeout`、`fetch` 等异步操作 - **维持高响应性**:避免同步代码阻塞主线程 - **调度任务优先级**:通过宏任务与微任务机制保证执行顺序 #### 二、核心组件与工作原理 Event Loop 的运行依赖三个关键组件,工作流程如下图所示: ```mermaid graph LR A[调用栈] --> B[执行同步代码] B --> C{栈空?} C -->|是| D[检查微任务队列] D --> E[执行所有微任务] E --> F[检查宏任务队列] F -->|有任务| G[取一个宏任务入栈] G --> B C -->|否| H[继续执行同步代码] ``` ##### 1. 调用栈(Call Stack) - **功能**:存储同步代码的执行上下文(函数调用) - **特点**:后进先出(LIFO)结构 - **示例**: ```javascript function first() { second(); // 入栈 console.log(1); } function second() { console.log(2); // 栈顶先执行 } first(); // 初始入栈 ``` 输出顺序:`2 → 1`(栈空后继续事件循环) ##### 2. 任务队列(Task Queues) 分为两种优先级不同的队列: | 队列类型 | 包含任务 | 优先级 | 触发时机 | |----------|------------------------|--------|------------------------| | **微任务队列** | `Promise.then`、`MutationObserver` | 最高 | 调用栈清空后立即执行 | | **宏任务队列** | `setTimeout`、`setInterval`、`I/O` | 次高 | 微任务队列清空后执行 | ##### 3. Event Loop 工作流程 1. **同步代码执行**:优先执行调用栈中所有同步代码 2. **检查微任务队列**:调用栈清空后,执行**所有**微任务(直到队列空) 3. **渲染更新**(浏览器):执行 DOM 渲染(如需要) 4. **处理宏任务**:取**一个**宏任务执行,完成后回到步骤2 5. **循环监测**:持续重复上述过程 #### 三、关键特性与示例 ##### 1. 微任务优先机制 ```javascript console.log("同步1"); setTimeout(() => console.log("宏任务"), 0); Promise.resolve() .then(() => console.log("微任务1")) .then(() => console.log("微任务2")); console.log("同步2"); /* 输出顺序: 同步1 → 同步2 → 微任务1 → 微任务2 → 宏任务 */ ``` ##### 2. 异步任务调度原则 - **宏任务间隔**:`setInterval` 需等待前次回调完成才执行下一次 - **最小延迟**:嵌套 `setTimeout` 在浏览器中最小延迟 4ms - **队列阻塞风险**:长时间同步代码会延迟异步任务执行 #### 四、浏览器与 Node.js 差异 | **平台** | 差异点 | |------------|-----------------------------------------------------------------------| | **浏览器** | 微任务在渲染前执行,`requestAnimationFrame` 作为特殊宏任务 | | **Node.js**| 使用 libuv 库,包含6个阶段(如 timers/poll/check),微任务在阶段间执行 | > 关键原则:**微任务队列必须在宏任务执行前清空**,这是保证 `Promise` 等异步操作及时响应的基础[^2]。 #### 五、为什么需要 Event Loop? JavaScript 的单线程特性要求: 1. **避免阻塞**:同步代码执行时不处理 I/O 2. **高效调度**:通过队列管理海量异步任务 3. **响应交互**:保证用户操作(点击等)能插队执行 > 正如引用[3]所述:*“异步代码完成后需要等待程序空闲,事件循环就是处理同步和异步操作的机制”*[^3]。 --- ### 相关问题 1. 宏任务和微任务的具体类型有哪些?它们的优先级如何排序? 2. 如何用 `Promise` 和 `setTimeout` 验证微任务优先于宏任务? 3. Node.js 中 Event Loop 的 `setImmediate` 和 `process.nextTick` 有何特殊之处? 4. 长时间运行的同步代码会对 Event Loop 产生什么负面影响?如何优化? [^1]: 前端面试大全:Event Loop [^2]: 什么是事件循环(Event Loop)?它是如何工作的? [^3]: JavaScript 异步处理机制 [^4]: Event Loop 执行上下文与调用栈
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值