目录
- 概述
- 1. 堆栈(Call Stack)
- 2. 堆(Heap)
- 3. 事件队列(Event Queue)
- 4. 宿主环境(Host Environment)
- 事件循环(Event Loop)
- 微任务和宏任务(Microtasks and Macrotasks)
- 事件循环的重要性
概述
JavaScript
的事件循环是一种机制,它允许 JavaScript
引擎在执行异步代码时,仍然保持单线程执行环境。因为 JavaScript
最初是为了与用户界面交互以及操作文档对象模型(DOM
)而设计的,所以处理异步事件——如用户输入、定时器、网络请求——是非常关键的。
事件循环的工作方式如下:
1. 堆栈(Call Stack)
JavaScript
引擎有一个称为“调用堆栈”的结构,用来追踪正在运行的所有执行上下文。当脚本开始执行时,全局执行上下文(比如,你的主JavaScript
文件)进入堆栈。然后,函数调用会被添加到堆栈的顶部。一旦函数执行完成,它就会从堆栈中弹出,控制权回到下面的上下文。
2. 堆(Heap)
这是内存的一部分,用于存储对象实例和闭包等动态分配的数据。
3. 事件队列(Event Queue)
事件队列(或任务队列)是异步事件的待办列表。这些事件可能是用户操作、网络事件、定时器到期等产生的。一旦堆栈清空,事件循环就开始工作,检查事件队列的第一个事件。
4. 宿主环境(Host Environment)
宿主环境(Host Environment)指的是JavaScript
代码执行的上下文环境,这个环境提供了额外的API和对象供JavaScript代码使用,但这些并不是JavaScript
语言本身的一部分。不同宿主环境下,JavaScript
能做的事情大相径庭。JavaScript
最初被设计为在浏览器中运行,但现在已经超出了这个范围。本次的宿主环境主要就是浏览器环境,其他诸如Node.js
、Electron
、React Native
等。
事件循环(Event Loop)
事件循环的主要职责是监视调用堆栈和事件队列。如果调用堆栈是空的(意味着没有正在执行的代码),事件循环就会取出事件队列中的第一个事件,并将与之关联的回调推送到调用堆栈中去执行。
以下是事件循环的简化模型:
- 执行同步代码,这些代码加入到调用堆栈中。
- 执行任何微任务(
microtask
),比如Promise
回调。 - 需要渲染的话,在这个阶段可能会执行渲染更新(在浏览器中)。
- 如果事件队列中有等待的宏任务(
macrotask
),如setTimeout
、setInterval
或I/O
操作,将第一个宏任务推送到调用堆栈。 - 返回第二步,循环这个过程。
事件循环(EventLoop):掌握后知道 JS 是如何安排和运行代码的
请回答下面 2 段代码打印的结果,并说明原因
console.log(1)
setTimeout(() => {console.log(2)
}, 2000)
console.log(1)
setTimeout(() => {console.log(2)
}, 0)
console.log(3)
1.作用:事件循环负责执行代码,收集和处理事件以及执行队列中的子任务
2.原因:JavaScript
单线程(某一刻只能执行一行代码),为了让耗时代码不阻塞其他代码运行,设计了事件循环模型
3.概念:执行代码和收集异步任务的模型,在调用栈空闲,反复调用任务队列里回调函数的执行机制,就叫事件循环
/*** 目标:阅读并回答执行的顺序结果
*/
console.log(1)
setTimeout(() => {console.log(2)
}, 0)
console.log(3)
setTimeout(() => {console.log(4)
}, 2000)
console.log(5)
微任务和宏任务(Microtasks and Macrotasks)
在事件循环中还区分了微任务(microtask
)和宏任务(macrotask
)。
- 宏任务是引擎处理的任务的主要单元,例如
setTimeout
、setInterval
、I/O
和UI
渲染等,由浏览器环境执行的异步代码。 - 微任务是需要在当前执行上下文结束后立即执行的任务,但在执行下一个宏任务之前。例如,
Promise
的回调就属于微任务,由 JS 引擎环境执行的异步代码。
微任务总是在当前宏任务结束后,以及在取出并执行下一个宏任务之前执行。微任务队列会一直执行,直到清空为止。这意味着微任务可以连续不断地添加和执行新的微任务,它们将在下一个宏任务之前执行,这可能会导致长时间不运行宏任务。
宏任务和微任务具体划分:
任务(代码) | 执行所在环境 |
---|---|
JS脚本执行事件 (script) | 浏览器 |
setTimeout/setInterval | 浏览器 |
AJAX请求完成事件 | 浏览器 |
用户交互事件等 | 浏览器 |
Promise对象.then() | JS 引擎 |
Promise
本身是同步的,而then
和catch
回调函数是异步的
事件循环模型
具体运行效果,参考动画或者视频
/*** 目标:阅读并回答打印的执行顺序
*/
console.log(1)
setTimeout(() => {console.log(2)
}, 0)
const p = new Promise((resolve, reject) => {resolve(3)
})
p.then(res => {console.log(res)
})
console.log(4)
注意:宏任务每次在执行同步代码时,产生微任务队列,清空微任务队列任务后,微任务队列空间释放!
下一次宏任务执行时,遇到微任务代码,才会再次申请微任务队列空间放入回调函数消息排队
总结:一个宏任务包含微任务队列,他们之间是包含关系,不是并列关系
事件循环的重要性
事件循环使得JavaScript
可以执行非阻塞代码,提供了一种在单线程环境中处理并行操作的方法。这就是为什么即使JavaScript
引擎在其核心是单线程,它仍然能够处理像Web
服务器那样的高并发工作。理解事件循环及其各个部分是高效地写出性能良好,并发行为正确的JavaScript
代码的关键。