JS中的事件循环(Event loop)

一、前言

本文章将带大家了解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

下面一张完整的事件循环的流程图,有助于更好的理解事件循环

完整细致的循环过程: 
  1. 主线程执行第一个宏任务
  2. 遇到同步操作直接执行并弹出栈
  3. 遇到异步操作时,交给宿主环境异步执行,执行成功后将回调函数推入任务队列。若执行的是宏任务则推入宏任务队列,否则推入微任务队列
  4. 当调用栈执行完第一个宏任务的所有代码,首先将微任务队列中的代码推入调用栈执行,所有微任务执行完毕之后
  5. 再检查并将一个宏任务推入调用栈执行
  6. 以上过程会不断重复,直到宏任务队列全部执行完毕

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

四、总结

  1. 同步任务:立即执行,按顺序进入调用栈。
  2. 异步任务:交给宿主环境处理,完成后进入任务队列。
  3. 事件循环:负责协调调用栈与任务队列的执行,确保任务按顺序执行。
  4. 微任务优先级高于宏任务:每个宏任务执行完后,都会先执行微任务。

通过事件循环,JavaScript 实现了在单线程的情况下处理异步任务,使得它可以高效应对异步密集型操作和用户交互。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值