JavaScript 解释器如何将全局语句添加到事件队列?

How Does the JavaScript Interpreter add Global Statements to the Event Queue?

我不确定全局范围内的语句是如何放入 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

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

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

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

我的脑子里显然有某种事件队列逻辑错误,但我一直在阅读我能阅读的所有内容,但我被卡住了。非常感谢任何帮助。

如果我们分解第二种情况,每个函数都是独立的,我们最终会得到

const handleResolved = (data) => {
  console.log(data);
}
const promiseBody = (resolve, reject) => setTimeout( innerPromiseTimeout, 0, resolve );
const innerPromiseTimeout = (resolve) => setTimeout( resolveWith1, 0, resolve );
const resolveWith1 = (resolve) => resolve("1");
const timeoutLog2 = () => {
  console.log("2");
};

// beginning of execution
// timers stack: [ ]

// promiseBody is executed synchronously
let p = new Promise( promiseBody );
// timers stack: [ innerPromiseTimeout ]

// this will happen only after resolveWith1 is called
p.then( handleResolved );
// timers stack: [ innerPromiseTimeout ]

setTimeout( timeoutLog2, 0 );
// timers stack: [ innerPromiseTimeout, timeoutLog2 ]

// some time later, innerPromiseTimeout is called
// timers stack: [ timeoutLog2, resolveWith1 ]

// timeoutLog2 is called
// timers stack: [ resolveWith1 ]

// resolveWith1 is called and then is executed in next microtask checkpoint
// timers stack: [ ]

另请注意,setTimeout在Chrome中仍有至少1ms(他们很快就会将其删除,但暂时存在),所以不要'假设 setTimeout(fn,0) 将作为下一个任务执行

我认为你的问题中有几处令人困惑的地方显示出对正在发生的事情的一些误解,所以让我们首先介绍一下。

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

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

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

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

第三,对于每种类型的事件,不只有一个事件队列。实际上有多个队列,如果有多种不同类型的事物等待 运行,则有关于先到达 运行 的规则。例如,当一个 promise 被 resolved 或 rejected 并因此注册了要调用的回调时,这些 promise 作业会在定时器相关回调之前到达 运行。 Promise 实际上有自己单独的队列,用于等待调用其适当回调的已解决或拒绝的 promises。

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

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

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

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

What I don't understand is the order in which things are added to the event queue. The first snippet implies that the promise p will run, and then during its execution, resolve is put in the event queue. Once all of p's stack frames are popped, then resolve is run. After than p.then(...) is run, and finally the last console.log("2");

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


In the second example, somehow the number 2 is being printed to the console before the number 1. But would things not be added to the event queue in this order

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

我们的步骤与第一个示例基本相同,只是不同之处在于:

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

你有这个:

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

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