首页 > 解决方案 > JavaScript解释器如何将全局语句添加到事件队列中?

问题描述

我不确定全局范围内的语句如何放入 JavaScript 事件队列中。我首先认为解释器通过并将所有全局语句逐行添加到事件队列中,然后执行并执行每个事件,但该逻辑与下面给出的示例不符。JavaScript 解释器如何将全局语句添加到事件队列中,为什么下面给出的两个示例的输出不同?

let handleResolved = (data) => {
  console.log(data);
}

let p = new Promise((resolve, reject) => {
      setTimeout(() => {resolve("1")}, 0)
});

p.then(handleResolved);

setTimeout(() => {
  console.log("2");
}, 0);

上面代码的控制台输出是

1
2

现在考虑这个例子。在这里,不同之处在于 promise 回调的主体,因为有一个嵌套setTimeout

let handleResolved = (data) => {
  console.log(data);
}

let p = new Promise((resolve, reject) => {   
  setTimeout(() = > {setTimeout(() => {resolve("1")}, 0)}, 0);
});

p.then(handleResolved);

setTimeout(() => {
  console.log("2");
}, 0);

上面代码的控制台输出是

2
1

我不明白的是添加到事件队列中的顺序。第一个片段暗示promise p 将运行,然后在其执行期间,resolve 被放入事件队列中。一旦p's弹出所有堆栈帧,然后resolve运行。在比p.then(...)运行之后,最后是最后一个console.log("2");

在第二个示例中,以某种方式将 number 在2number 之前打印到控制台1。但是事情不会按这个顺序添加到事件队列中吗

1.) p
2.) setTimeout( () => {resolve("1")}, 0)
3.) resolve("1")
4.) p.then(...)
5.) console.log("2")

我的脑海中显然有某种事件队列逻辑错误,但我一直在阅读我能阅读的所有内容,但我被卡住了。对此的任何帮助将不胜感激。

标签: javascriptpromisesettimeouteventqueue

解决方案


您的问题中有一些令人困惑的事情,我认为这些事情表明对正在发生的事情有一些误解,所以让我们首先介绍一下。

首先,“语句”从未放入事件队列中。当异步任务完成运行或定时器运行时,将在事件队列中插入一些内容。在此之前没有任何东西在队列中。在您调用之后setTimeout(),在触发时间之前,setTimeout()事件队列中没有任何内容。

而是setTimeout()同步运行,在 JS 环境内部配置一个定时器,关联你传递给的回调函数setTimeout()到那个计时器,然后立即返回 JS 在下一行代码继续执行的位置。稍后,当达到计时器触发的时间并且控制返回到事件循环时,事件循环将调用该计时器的回调。其工作原理的内部结构会根据其所处的 Javascript 环境而有所不同,但相对于 JS 环境中发生的其他事情,它们都具有相同的效果。例如,在 nodejs 中,实际上没有将任何内容插入事件队列本身。相反,事件循环有几个阶段(需要检查不同的事情以查看是否有一些东西要运行),其中一个阶段是检查当前时间是否在安排下一个计时器事件的时间或之后(已安排的最快计时器)。在nodejs中,计时器存储在一个排序的链表中,最快的计时器位于链表的头部。事件循环将当前时间与列表头部计时器上的计时器进行比较,以查看是否有时间执行该计时器。如果没有,它会在各种队列中寻找其他类型的事件。如果是这样,它会获取与该计时器关联的回调并调用该回调。

其次,“事件”是导致回调函数被调用并执行该回调函数中的代码的事物。

调用可能会立即或稍后(取决于函数)将某些内容插入事件队列的函数。因此,当setTimeout()执行时,它会安排一个计时器,一段时间后,它将导致事件循环调用与该计时器关联的回调。

第三,每种类型的事件不只有一个事件队列。实际上有多个队列,并且如果有多种不同类型的事物等待运行,则有关于首先运行什么的规则。例如,当一个 Promise 被解决或拒绝并因此注册了要调用的回调时,这些 Promise 作业将在与计时器相关的回调之前运行。Promise 实际上有自己独立的队列,用于等待调用相应回调的已解决或已拒绝的 Promise。

第四,setTimeout()即使给定0时间,也总是在事件循环的某个未来滴答声中调用它的回调。它从不同步或立即运行。因此,当前 Javascript 执行线程的其余部分总是在setTimeout()回调被调用之前完成运行。在当前执行线程完成并且控制返回到事件循环之后,Promise 也总是调用.then()或处理程序。.catch()事件队列中的待处理承诺操作总是在任何待处理的计时器事件之前运行。

并且稍微混淆一下,Promise 执行器函数(fn你在 中传递的回调new Promise(fn))确实是同步运行的。事件循环不参与在fn那里运行。 new Promise()被执行并且那个promise构造函数立即调用你传递给promise构造函数的executor回调函数。

现在,让我们看看您的第一个代码块:

let handleResolved = (data) => {
  console.log(data);
}

let p = new Promise((resolve, reject) => {
      setTimeout(() => {resolve("1")}, 0)
});

p.then(handleResolved);

setTimeout(() => {
  console.log("2");
}, 0);

按顺序,这是这样做的:

  1. 为变量分配一个函数handleResolved
  2. 立即同步运行您传递给new Promise()它的承诺执行器回调的调用。
  3. 该执行器回调,然后调用setTimeout(fn, 0)它安排一个计时器很快运行。
  4. new Promise()构造函数的结果p赋给变量。
  5. Executep.then(handleResolved)仅注册handleResolved为当 promisep被解决时的回调函数。
  6. 执行第二个setTimeout(),它安排一个计时器很快运行。
  7. 将控制权返回给事件循环。
  8. 在将控制权返回给事件循环后不久,您注册的第一个计时器就会触发。由于它与您注册的第二个具有相同的执行时间,因此这两个计时器将按照它们最初注册的顺序执行。因此,第一个调用它的回调,该回调调用resolve("1")导致承诺p改变其状态以得到解决。这.then()通过将“作业”插入到承诺队列中来安排该承诺的处理程序。该作业将在当前堆栈帧完成执行并将控制权返回给系统后运行。
  9. 对完成和控制的调用resolve("1")返回到事件循环。
  10. 因为挂起的承诺操作在挂起的计时器之前提供,handleResolved(1)所以被调用。该函数运行,向控制台输出“1”,然后将控制权返回给事件循环。
  11. 然后事件循环调用与剩余计时器关联的回调,并将“2”输出到控制台。

我不明白的是添加到事件队列中的顺序。第一个片段暗示promise p 将运行,然后在其执行期间,resolve 被放入事件队列中。一旦 p 的所有堆栈帧都被弹出,然后解析运行。之后 p.then(...) 运行,最后是最后一个 console.log("2");

我真的不能直接对此做出回应,因为这根本不是事情的运作方式。承诺不会“运行”。构造new Promise()函数运行。Promise 本身只是通知机器,用于通知已注册的侦听器其状态的变化。 resolve未放入事件队列。 resolve()是一个被调用的函数,当它被调用时会改变 Promise 的内部状态。 p没有堆栈帧。 p.then()立即运行,而不是稍后运行。只是p.then()所做的只是注册一个回调,以便以后可以调用回调。请参阅上面的 1-11 步骤,了解事情如何工作的顺序。


在第二个示例中,不知何故,数字 2 在数字 1 之前被打印到控制台。但是事情不会按此顺序添加到事件队列中

在第二个示例中,您有三个调用,setTimeout()其中第三个嵌套在第一个内部。这就是改变你相对于第一个代码块的时间的原因。

我们的步骤与第一个示例基本相同,但不同的是:

setTimeout(() => {resolve("1")}, 0)

你有这个:

setTimeout(() = > {setTimeout(() => {resolve("1")}, 0)}, 0);

这意味着调用了 promise 构造函数并设置了这个外部计时器。然后,其余的同步代码运行,然后设置代码块中的最后一个计时器。就像在第一个代码块中一样,第一个计时器将在第二个计时器之前调用它的回调。但是,这次第一个只是调用另一个setTimeout(fn, 0)。由于计时器回调总是在事件循环的某个未来滴答声中执行(不是立即执行,即使时间设置为0),这意味着第一个计时器在有机会运行时所做的就是安排另一个计时器。然后,代码块中的最后一个计时器开始运行,您会2在控制台中看到 。然后,完成后,第三个计时器(嵌套在第一个计时器中的那个)开始运行,您会1在控制台中看到 。


推荐阅读