一、前言
本文章将带大家了解js代码的执行机制,也就是事件循环,还有我个人对同步与异步、调用栈、宏任务和微任务等概念的理解。
二、前置知识
2.1 同步与异步
同步(Synchronous)
- 定义:同步代码按顺序执行,一行接一行,当前任务完成后才会继续执行下一行代码。
- 阻塞:同步任务会阻塞后续代码的执行,必须等待当前任务完成。
- 适用场景:适合处理简单的、不会耗费大量时间的任务,如简单的计算、DOM操作等。
- 生活例子:当你做饭时,假设你先要煮米饭,然后才能炒菜。你必须等米饭完全煮好后,才能开始炒菜。这个过程是按顺序进行的,也就是同步操作,不能同时完成两件事。
异步(Asynchronous)
- 定义:异步代码不会阻塞主线程的执行。代码可以发起异步任务,在任务执行期间可以继续处理其他代码,而不用等待任务完成。
- 非阻塞:异步任务在执行过程中不会阻止其他任务的执行,常用于处理需要较长时间才能完成的任务,例如网络请求、文件读取等。
- 适用场景:当需要处理 I/O 操作、定时器、API 请求、事件监听等可能需要一定时间完成的任务时,异步执行更合适。
- 生活例子:当你做饭时,假设你先煮米饭,而煮米饭这项任务是耗时的,煮饭开始后你需要在旁边一直等到他煮完,你可以在煮米饭的这段时间,去洗菜、切菜和炒菜,当米饭熟了之后,电饭煲会提示你的。煮米饭这个过程就是异步的,煮饭这件事不会影响你做别的事。
2.2 栈与队列
栈(Stack)和队列(Queue)是两种常见的数据结构,借助这两个概念可以更好的理解Javascript 如何通过事件循环机制处理同步与异步代码。
栈(Stack)
- 定义:栈是一种 后进先出(LIFO, Last In First Out)的数据结构。即最后一个进入栈的数据项最先被移出。
队列(Queue)
- 定义:队列是一种 先进先出(FIFO, First In First Out)的数据结构。即最先进入队列的元素最先被移出。
三、事件循环
3.1 什么是事件循环
事件循环(Event Loop)是 JavaScript 中用来处理异步任务的机制,确保非阻塞的任务执行。尽管 JavaScript 是单线程语言,事件循环使它可以执行异步任务并保持流畅的用户体验。
事件循环的核心概念
- 单线程:JavaScript 的执行是单线程的,所有代码都在一个线程中运行。
- 同步任务与异步任务:同步任务会立即执行,而异步任务(如
setTimeout
、网络请求)不会阻塞主线程,它们的回调会推迟到稍后执行。 - 调用栈(Call Stack):用于管理同步任务。每当 JavaScript 执行一个函数调用,它会被压入调用栈,执行完毕后弹出。
- 任务队列(Task Queue):异步任务的回调函数会被放入任务队列,等待调用栈清空后执行。
- 事件循环:事件循环不断检查调用栈是否为空,如果为空,它会从任务队列中取出任务并执行。
我画了一张事件循环的流程图,有助于更好的理解事件循环
这个过程确保了 JavaScript 可以在执行同步代码的同时处理异步任务,并且避免了阻塞。事件循环使得 JavaScript 在单线程的环境中依然能够进行高效的异步操作。
3.2 任务队列
任务队列又可以分为宏任务队列(Macro Task Queue)和微任务队列(Micro Task Queue),它们的优先级和执行方式不同。
宏任务队列(Macro Task Queue)
宏任务一般是由宿主环境提供的处理异步代码的方法,每次事件循环首先检查并执行一个宏任务。
常见的宏任务有:
- setTimeout、setInterval
- 网络请求
- DOM事件
微任务队列(Micro Task Queue)
微任务一般是由JS引擎提供的处理异步代码的方法,微任务在当前宏任务处理完后立即执行,优先级高于下一个宏任务。
常见的微任务有:
- Promise.then、Promise.catch、Promise.finally
- process.nextTick
下面一张完整的事件循环的流程图,有助于更好的理解事件循环
完整细致的循环过程:
- 主线程执行第一个宏任务
- 遇到同步操作直接执行并弹出栈
- 遇到异步操作时,交给宿主环境异步执行,执行成功后将回调函数推入任务队列。若执行的是宏任务则推入宏任务队列,否则推入微任务队列
- 当调用栈执行完第一个宏任务的所有代码,首先将微任务队列中的代码推入调用栈执行,所有微任务执行完毕之后
- 再检查并将一个宏任务推入调用栈执行
- 以上过程会不断重复,直到宏任务队列全部执行完毕
3.3 具体实例
分析一下这个简单例子,加深一下理解:
<script>
console.log('1');
setTimeout(() => {
console.log('2');
}, 1000);
console.log('3');
new Promise((resolve) => {
resolve();
}).then(() => {
console.log('4');
}).then(() => {
console.log('5');
});
console.log('6');
// 执行结果
// 1
// 3
// 6
// 4
// 5
// 2
</script>
- 将第一个宏任务<script></script>推入调用栈执行
- 将所有同步代码执行输出 1 3 6
- 遇到定时器,交给宿主环境处理,一秒钟到了后推入宏任务队列
- 遇到Promise对象,同步执行里面的代码,里面调用了promise成功的函数
- 将then的回调函数推入微任务队列
- 所有同步代码执行完后,将微任务队列中的任务推入调用栈执行输入 4 5
- 微任务队列为空后,开启下一轮循环
- 执行一个宏任务输入 2
- 最终输出结果 1 3 6 4 5 2
四、总结
- 同步任务:立即执行,按顺序进入调用栈。
- 异步任务:交给宿主环境处理,完成后进入任务队列。
- 事件循环:负责协调调用栈与任务队列的执行,确保任务按顺序执行。
- 微任务优先级高于宏任务:每个宏任务执行完后,都会先执行微任务。
通过事件循环,JavaScript 实现了在单线程的情况下处理异步任务,使得它可以高效应对异步密集型操作和用户交互。