浅谈事件循环Event Loop

本文介绍JavaScript的事件循环机制,解释单线程如何处理异步任务。包括执行栈、消息队列及事件循环流程,通过实例说明任务执行顺序。

前言

  1. 首先,JavaScript是一个单线程的脚本语言。

    JavaScript是单线程的,可以说这是JavaScript最核心也是最基本的特性。单线程就意味着同一时间只能干一件事

  2. 其次,JavaScript需要异步

    如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验。

    但JavaScript虽然是单线程,但是完全可以模拟多线程(借助于浏览器这个宿主来实现),靠的就是 事件循环 Event Loop。所以JS中存在异步执行。

事件循环:

  • 是单线程程序在处理异步事件时进行的一种循环过程。

  • 对于异步事件,程序会先将其加入到事件队列中挂起,等主线程空闲时会去执行事件队列中的事件。这个过程会不断重复,形成一个循环。

JavaScript 是单线程语言,通过 事件循环(Event Loop) 实现异步编程。
事件循环的核心由 宏任务队列(MacroTask Queue)微任务队列(MicroTask Queue) 组成,两者的执行顺序直接影响代码行为。


事件循环机制

1. 执行栈

当JavaScript开始执行代码时,会按顺序逐行执行,并形成一个执行栈。

执行栈是一个后进先出(LIFO)的数据结构,用于存储函数调用。

在执行过程中如果遇到:

  • 同步任务:会按照代码顺序立即执行
  • 异步任务:不会立即执行,而是会将这些任务放入 Event Table(事件表)中注册并等待执行。同时,继续执行后续的同步任务,不会阻塞主线程的执行。

直到所有的同步任务执行完毕,即主线程(执行栈)空闲

2. 消息队列

异步任务在 Event Table 中注册函数,当异步任务的回调函数被触发时,Event Table 会将这些任务推入 Event Queue(事件队列)中等待执行。异步任务又分为微任务和宏任务:

宏任务

宏任务是JavaScript中的最外层任务,通常包括一些执行时间较长、可能阻塞线程的操作。

特点:

  • 宏任务由浏览器或Node.js环境发起的任务,会在每轮事件循环中依次执行,每次事件循环从宏任务队列中取 一个 任务执行。
  • 宏任务队列可以有多个,JavaScript引擎会按照先进先出的顺序处理。

宏任务会被添加到宏任务队列(MacroTask Queue)中,常见的宏任务有:

  • <script> 标签整体代码 (全局代码的执行本身是一个宏任务)
  • setTimeoutsetInterval(定时器回调)
  • I/O 操作(如文件读写、网络请求等)
  • UI 渲染:浏览器在更新DOM或样式后,会触发重绘(Repaint)和回流(Reflow)等UI渲染过程
  • 事件回调:如点击、滚动等用户交互事件

.

微任务

微任务是在当前宏任务执行完毕后、下一个宏任务开始前立即执行的一类任务。

特点:

  • 微任务由 JavaScript 自身发起的任务,每个宏任务执行完毕后会立即清空所有微任务队列。微任务通常用于处理一些比宏任务优先级更高的操作,确保任务能够快速完成。
  • 微任务队列只有一个,所有微任务会按照添加的顺序依次执行。

微任务会被添加到微任务队列(MicroTask Queue)中,常见的微任务有:

  • Promise 的回调函数(即 thencatchfinally 中的函数)
  • MutationObserver 用于观察DOM树的变化,并在变化发生时执行回调函数(DOM 变动监听回调)
  • process.nextTick(Node.js 特有,优先级高于其他微任务)
  • queueMicrotask 一个直接将任务添加到微任务队列的函数,适用于需要在当前事件循环周期内尽快执行的任务(显式添加微任务的 API)
宏任务(MacroTask)微任务(MicroTask)
定义JavaScript 中的最外层任务,执行时间较长,可能阻塞线程在当前宏任务执行完毕后、下一个宏任务开始前立即执行的任务
特点- 由浏览器或 Node.js 环境发起
- 每轮事件循环中依次执行一个
- 宏任务队列可以有多个,先进先出顺序处理
- 由 JavaScript 自身发起
- 每个宏任务执行完毕后会立即清空所有微任务队列
- 微任务队列只有一个,按照添加顺序依次执行
常见类型- <script> 标签整体代码
- setTimeoutsetInterval(定时器回调)
- I/O 操作(如文件读写、网络请求等)
- UI 渲染(重绘、回流等)
- 事件回调(如点击、滚动等用户交互事件)
- Promise 的回调函数(thencatchfinally
- MutationObserver(DOM 变动监听回调)
- process.nextTick(Node.js 特有)
- queueMicrotask(显式添加微任务的 API)
队列先进先出 FIFO先进先出 FIFO

这个表格清晰地展示了宏任务和微任务的定义、特点以及常见类型,方便进行对比和理解。

微任务和宏任务是分开存储在不同的队列中的,但它们都通过事件循环来调度和执行。

3. 事件循环

当所有的同步任务执行完毕,即主线程(执行栈)空闲时,事件循环(Event Loop)会检查 Event Queue(消息队列)中是否有待执行的异步任务。

如果有,事件循环会取出一个异步任务(通常是消息队列中的第一个任务)放入主线程执行栈中执行:

  • JavaScript引擎会先查看微任务队列是否有任务。如果有,就执行微任务队列中的所有任务,然后再继续查看微任务队列,直到微任务队列为空。

  • 执行完所有的微任务后,从宏任务队列中取出下一个宏任务,取出到执行栈中执行。

  • 这个过程会不断重复,从而形成“事件循环”。

在一个事件循环的迭代中,微任务总是先于宏任务执行,会先执行完所有的微任务,然后才会执行下一个宏任务。并且在一个宏任务执行完毕后,会立即检查并执行微任务队列中的所有任务。

执行的顺序是 执行栈中的代码 => 微任务 => 宏任务 => 微任务 => 宏任务 => ...

事件循环是 JavaScript 运行时不断监视执行栈和Event Queue(消息队列)的过程,负责协调同步任务和异步任务的执行

流程图

在这里插入图片描述

总结

综上所述,JavaScript 的事件循环机制负责协调同步任务和异步任务的执行。在主线程空闲时,事件循环会从消息队列中取出一个异步任务放入主线程中执行。在执行异步任务的过程中,可能会产生新的微任务,这些微任务会被放入微任务队列中等待处理。事件循环会优先处理微任务队列中的所有微任务,然后再处理消息队列中的一个消息。这样可以确保 JavaScript 代码的执行顺序符合预期。
在这里插入图片描述


问题

  1. 微任务中又产生了新的微任务,新产生的微任务会什么时候执行?

    当前的微任务执行完后,事件循环会接着从微任务队列的头部取出下一个微任务来执行,直至微任务队列为空,然后才会去执行下一个宏任务。

    新产生的微任务会在当前微任务队列的末尾排队,在当前这一批微任务依次执行完毕前执行,而不是等到下一个宏任务执行时才执行。

  2. 微任务中又产生了新的宏任务,新产生的宏任务会什么时候执行?

    1. 新宏任务会被添加到宏任务队列的末尾。
    2. 继续执行当前微任务队列中的剩余微任务,直到所有微任务执行完毕。
    3. 事件循环才会从宏任务队列中取出下一个宏任务执行。
    4. 由于新产生的宏任务是后加入队列的,会在队列中已有的其他宏任务之后执行(宏任务会按照队列中 先进先出(FIFO) 的顺序依次执行)
      .

    新的宏任务会被添加到宏任务队列中,但不会立即执行。它会等到当前微任务队列全部执行完毕,并且下一个宏任务开始时才会被处理。


举例

例1:

console.log(1)

setTimeout(function(){
    console.log(2)
},0)

console.log(3)

运行结果是: 1 3 2

它的执行顺序:

  1. console.log(1) 是同步任务,放入主线程里
  2. setTimeout() 是异步任务,被放入event table, 0 秒之后被推入event queue
  3. console.log(3) 是同步任务,放到主线程里
  4. 先执行完所有的同步任务,即先打印出 13
  5. 执行栈空闲时再去 event queue 里取出一个异步任务放入主线程执行栈中执行,即打印出 2

.

例2:

setTimeout(function(){
     console.log(1)
 });
 
 new Promise(function(resolve){
     console.log(2);
     resolve();
 }).then(function(){
     console.log(3)
 });
 
 console.log(4);

运行结果是: 2 4 3 1

也就是说 promise 在 setTimeout 之前执行了。

对例2的分析:

  1. 首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
  2. 遇到 new Promise直接执行,打印 “2”
  3. 遇到then方法,是微任务,将其放到微任务的【队列里】
  4. 遇到console.log(4),同步任务,直接打印“4”
  5. 打印 “代码执行结束”
  6. 本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"3"
  7. 到此,本轮的event loop全部完成。
  8. 下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个setTimeout里的函数,执行打印"1"
  9. 所以最后的执行顺序是: 2 4 3 1

注意:

DOM事件也是基于Event Loop,但不是异步

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫老板的豆

你的鼓励将是我创作的最大动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值