您的问题中有一些令人困惑的事情,我认为这些事情表明对正在发生的事情有一些误解,所以让我们首先介绍一下。
首先,“语句”从未放入事件队列中。当异步任务完成运行或定时器运行时,将在事件队列中插入一些内容。在此之前没有任何东西在队列中。在您调用之后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);
按顺序,这是这样做的:
- 为变量分配一个函数
handleResolved
。
- 立即同步运行您传递给
new Promise()
它的承诺执行器回调的调用。
- 该执行器回调,然后调用
setTimeout(fn, 0)
它安排一个计时器很快运行。
- 将
new Promise()
构造函数的结果p
赋给变量。
- Execute
p.then(handleResolved)
仅注册handleResolved
为当 promisep
被解决时的回调函数。
- 执行第二个
setTimeout()
,它安排一个计时器很快运行。
- 将控制权返回给事件循环。
- 在将控制权返回给事件循环后不久,您注册的第一个计时器就会触发。由于它与您注册的第二个具有相同的执行时间,因此这两个计时器将按照它们最初注册的顺序执行。因此,第一个调用它的回调,该回调调用
resolve("1")
导致承诺p
改变其状态以得到解决。这.then()
通过将“作业”插入到承诺队列中来安排该承诺的处理程序。该作业将在当前堆栈帧完成执行并将控制权返回给系统后运行。
- 对完成和控制的调用
resolve("1")
返回到事件循环。
- 因为挂起的承诺操作在挂起的计时器之前提供,
handleResolved(1)
所以被调用。该函数运行,向控制台输出“1”,然后将控制权返回给事件循环。
- 然后事件循环调用与剩余计时器关联的回调,并将“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
在控制台中看到 。