Skip to content

一文搞懂 JS 中的事件循环

面试题:介绍一下 js 的事件循环机制(最经典的一版,彻底理解)

事件循环的介绍和它的作用:

JavaScript 事件循环(Event Loop)是一种用于管理异步代码执行的机制,确保在单线程的 JavaScript 运行环境中能够处理异步任务而不阻塞主线程事件循环使得 JavaScript 可以处理诸如异步回调、定时器、网络请求等任务,从而实现非阻塞的异步编程。

事件循环机制包括以下几个关键组成部分:

  1. 调用栈(Call Stack): 调用栈是一个存储函数调用的栈结构,用于管理当前正在执行的代码。当一个函数被调用时,其对应的执行上下文被推入调用栈中,函数执行完成后,对应的执行上下文从栈中弹出调用栈的运作方式使得 JavaScript 是单线程的

  2. 消息队列(Message Queue): 消息队列是用来存放异步任务的队列当异步任务执行完成后(在主线程执行,在Event Table中注册事件,不会阻塞同步代码),会将其对应的回调函数或任务放入消息队列中。 ——> 注意是异步任务执行完成之后(比如 axios 异步请求完毕了变成 fulfilled 状态了,或者 setTimeout 的时间到了),才将其回调推入消息队列。

  3. 事件表(Event Table): 事件表是一个概念,用于描述浏览器中事件的管理和调度。虽然不是官方术语,但它用于解释浏览器是如何处理事件以及执行相应的回调函数的。

    在浏览器中,事件可以是用户交互(比如点击、键盘输入等)、网络请求(比如 AJAX 请求的响应,即Promise对象的状态改变)、定时器到期(setTimeout时间到了)、DOM 变化等。这些事件在发生时,浏览器需要管理它们的触发和执行(事件触发之后将对应的回调函数放入对应的任务队列里面),以确保事件的处理不会影响页面的响应性。

    事件表的概念是为了说明浏览器是如何跟踪和管理不同类型事件的。它大致可以描述为以下步骤:

    1. 事件触发: 当事件发生时,浏览器会将事件添加到相应的事件队列中,具体根据事件类型分别加入不同的队列,比如点击事件会加入点击事件队列,定时器事件会加入定时器事件队列,等等。
    2. 事件循环: 浏览器通过事件循环(Event Loop)来管理这些事件队列。事件循环会持续运行,不断地检查是否有事件需要处理。如果事件队列中有事件等待处理,事件循环就会开始执行这些事件的回调函数。
    3. 回调执行: 当事件循环开始处理某个事件队列时,它会依次执行队列中的事件的回调函数。例如,点击事件队列中的点击事件发生后,就会触发相应的回调函数执行。

    需要注意的是,事件表的描述是一个简化的模型,实际上浏览器中的事件管理和处理涉及更多细节,比如异步操作、优先级、任务队列等。然而,通过理解事件表的概念,可以更好地理解浏览器是如何在不同事件之间切换和执行回调函数的。

    对于js代码里面的异步任务的一个流转过程的理解:在调用栈中直接运行异步任务代码(比如发送网络请求) ——> 在event table里面注册事件,等待触发 ——> 触发之后将回调函数放入消息队列 ——> 调用栈空了之后将消息队列的回调取出来进行执行!

  4. 事件循环(Event Loop): 事件循环是一个持续运行的机制,它会不断地从消息队列中取出任务,将其放入调用栈中执行。当调用栈为空时,事件循环会检查消息队列中是否有任务,如果有,则将任务放入调用栈执行。

    注意:使用setTimeout时候,明明设置成了 3 秒后执行,但是实际执行时候却是大于等于 3 秒的,为什么到了 3 秒后回调函数没有执行呢?——> 这就是由于事件循环机制,必须等待调用栈执行完毕了才可以!

开启事件循环并不断执行事件循环的基本流程如下:(一段 js 代码开始运行之后的逻辑过程)

  1. 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入 Event Table 并注册事件和回调函数。(js 在执行代码的时候遇到同步任务就直接执行即推入调用栈执行,遇到异步任务会执行并进入 Event Table 并注册事件[只有这样我们才知道这个异步任务啥时候执行完毕,啥时候应该执行回调],事件触发之后将回调推入异步队列
  2. 当指定的事件完成时(比如 ajax 请求完毕之后,或者 dom 事件被触发之后),Event Table 会将这个函数移入 Event Queue。
  3. 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行(将任务从队列中取出,推入调用栈中执行),先执行所有的微任务,然后进行 DOM 渲染,渲染完毕之后,触发事件循环,即开启下一次循环执行一个宏任务(执行一个之后就可以接着去找新的微任务了)。
  4. 上述第 3 个步骤会不断重复,也就是常说的 Event Loop(事件循环)

注:虽然说是循环,但是实际上第一次循环的时候就执行了所有 js 同步事件代码(页面初始化的时候),后面都是在不停的去执行微任务和宏任务这两种(第 3 个步骤),但是就算任务队列空了,事件循环也不会停止!

注意:上面所说的开启下一次循环,与其说开启下一次循环,不如说真正地开始了事件循环,其实我们所说的第一次事件循环根本就不算事件循环,真正的事件循环就是在第二次和以后才是事件循环!——>理解了这句话才能真正地搞懂事件循环

注意:在开启事件循环的过程中(即我们所说的第一次事件循环),只涉及微任务的执行,根本不涉及宏任务的执行!只有在真正的事件循环里面才执行宏任务,并且每次只执行一个!

在后面(第二次及以后)不停的去执行微任务和宏任务的过程中,可以简化为以下两个步骤:——> 这也就是本质上的事件循环

其实后面的本质上都是一些回调函数的逻辑了!

  1. 从宏任务队列中取出一个任务,执行其代码。
  2. 在执行下一个宏任务之前,先执行所有的微任务:检查微任务队列,依次执行微任务队列中的任务,直到微任务队列为空,开启下一次循环。

注意:在第一次事件循环我们可以说微任务先于宏任务执行(开启事件循环的过程),但是在第二次及以后我们看到的其实是宏任务先于微任务,并且第二次的宏任务是来自于第一次的事件循环的。

所以在js开始运行之后微任务确实是先于宏任务执行,但是一旦开启了真正的事件循环,宏任务就是先于微任务执行的!

为什么事件循环永远不会停止呢?

事件循环不会停下来,它会持续不断地运行,确保浏览器能够始终响应事件和执行回调函数。事件循环是浏览器保持响应性的关键机制之一(因为 js 是单线程的,没有事件循环就没办法执行回调了,js 就无法动态地做出反应了,即没有办法处理异步任务和事件回调)。

事件循环的基本原理是不断地从不同类型的事件队列中获取事件,并执行相应的回调函数。这包括宏任务队列(比如用户交互事件、定时器事件、网络请求的响应等)和微任务队列(比如 Promise 的 .then()async/await 等)。

注意:这里永远不会停止,指的是页面在初始化之后到销毁之前的过程中!

为什么对于 ajax 异步请求要不断重复事件循环呢?是怎么做到对于 ajax 的异步处理的?

拿异步请求举例,因为 Event Table 里面注册的事件不一定什么时候触发(这和 ajax 请求的响应时间有关系),当请求返回的时候才会把回调函数推入消息队列,所以说我们不一定在第几次的事件循环中才能去执行这个异步回调,所以必须是循环的,而不是只有一次!

事件循环整体流程图解:

下面这个图非常经典,WebApis 的位置就是 Event Table 执行栈!

image-20230812040723653

**异步任务(的回调函数)**分为宏任务和微任务

在事件循环中,异步任务分为宏任务(Macrotask)和微任务(Microtask):

  • 宏任务包括 setTimeoutsetIntervalrequestAnimationFrame、I/O 操作等。
  • 微任务包括 Promiseprocess.nextTick(在 Node.js 中)等。

事件循环会先处理所有微任务(开启事件循环的过程中),然后再处理一个宏任务,然后再处理所有微任务,依此类推。这保证了微任务能够在下一个宏任务之前被处理,从而提供更快的响应时间。

总之,JavaScript 的事件循环机制是保证异步编程正常运行的核心机制,通过调用栈、消息队列和事件循环协同工作,使得异步任务能够按照一定的顺序执行,从而避免了阻塞和死锁。

在这里插入图片描述

注意:DOM 事件处理函数也是宏任务(DOM 事件的绑定被视为异步任务)

异步任务的回调才需要区分宏任务和微任务,而异步任务都是统一需要被 Event Table 注册的!