异步 JavaScript 程序中的函数调用究竟是如何排序的?

How exactly are the function calls ordered in an asynchronous JavaScript program?

我正在 JavaScript (JS) 中学习 异步编程 的概念。但是,我很难理解这一点。这几天一直在网上看各种文章想弄明白,但就是领悟不到。

所以,我的疑惑是:

setTimeout(function(){ alert("Hello 1"); }, 3000); // .....(i)
console.log("Hi!");                              // .....(ii)
setTimeout(function(){ alert("Hello 2"); }, 2000); // .....(iii)
  1. 考虑上面的代码。我了解到 JS 使用调用堆栈和事件队列来命令执行指令。在上面的代码中,当 JS 解释器看到 (i) 行时,它会将 setTimeout 排入事件队列,然后移动到 (ii),将其放入调用 -堆栈,执行它,然后移动到 (iii),它再次将 setTimeout 排入事件队列(并且此队列 非空 ), 对吧?

  2. 如果我在上面的问题中写的是正确的,那么一旦我们到达代码的末尾,因为调用堆栈是空的 setTimeouts 排队到事件队列中,一个一个地执行,对吧? - 这意味着如果我们假设它花费了(比方说)10ms 到达代码的末尾,那么由于事件队列具有 setTimeout (i)在前面,它等待3s,然后在time = 3010ms弹出警报:“Hello 1”,出队它,并且类似地 setTimeout (iii) 被执行 在 2 秒后 然后警报:“Hello 2” 在 time = 5010ms,对吧?

  3. 让我们假设在 (i) 和 (iii) 处没有 setTimeouts,我们有 addEventListener() 的 带有一些回调函数。即使在这种情况下,事件侦听器的回调函数是否会在事件队列中排队?我觉得他们没有入队,因为我们可以在 (iii) 的回调之前触发 (iii) 的回调。那么,在这种情况下到底发生了什么?除了调用堆栈和事件队列之外,是否还有其他任何东西以某种方式存储有关它们的信息并相应地触发它们的回调?

简而言之-shell这些指令究竟是如何排序的?后台到底发生了什么?

如果能得到全面的回答,我将不胜感激。如果您还可以提供有关此主题的一些综合材料的链接,那就太好了

感谢您的帮助!

到目前为止你是正确的:

That means if we assume it took (say) 10ms to come to the end of the code, then since the event-queue has the setTimeout (i) in the front, it waits for 3s, then pops the alert: "Hello 1", at the time = 3010ms

setTimeout 将从 setTimeout 被调用 的特定时间 后将回调排队到 运行。例如,如果 setTimeout(fn, 3000) 是 运行,然后 5 秒的昂贵阻塞代码 运行s,fn 将立即 运行 在那 5 秒之后。如果 1 秒的阻塞代码改为 运行s,则 fn 将在该阻塞代码完成后 运行 2 秒。例如:

console.log('script start');
// Putting the below in a setTimeout so that the above log gets rendered

setTimeout(() => {
  setTimeout(() => {
    console.log('setTimeout callback');
  }, 1000);
  const t0 = Date.now();
  while (Date.now() - t0 < 700);
  console.log('loop done');
}, 30);

在上面,您可以看到 for 循环需要一些时间才能完成,但是一旦它完成,setTimeout 回调几乎紧随其后 运行。

您可以这样想:当 setTimeout 被调用时,在 Date.now() + delay,一个新任务被推送到宏任务队列。其他代码可能在任务被推送时 运行ning,或者在 setTimeout 之后的代码完成之前可能需要一些时间,但无论如何,回调将 运行 Date.now() + delay.

之后尽快

此过程在 the specification:

中有详细描述
  1. (After waiting is finished...) Queue a global task on the timer task source given method context to run task.

任务在时间过去之前不存在于队列(或堆栈)中,函数调用仅在任务开始后进入堆栈 运行ning - 时间一到,可能 就会发生,或者如果当时 运行 正在执行不同的任务,则可能需要一些额外的时间。


we had addEventListener()'s with some call-back functions. Even in this case, will the call-back functions of the event listeners be enqueued in the event-queue?

否 - 他们的处理程序只会在侦听器触发后才放入队列。

您现在可能已经知道 JavaScript 引擎在单个线程上执行,那么如何处理异步操作?您在以下陈述中部分正确,但还有更多内容:

Consider the above code. I learnt that JS uses a call-stack and an event-queue to order the execution of instructions.

没错,我们确实有一个 调用堆栈 和一个 事件循环 。但是我们还有一个WEB APIs环境Call-back队列和一个Micro-task队列.

每当有任何异步任务,它移动到WEB API环境,例如,当你有一个标签在“src”属性中有一个非常大的图像时,这个图像不会被同步下载,因为这会阻塞线程,而是将其移至加载图像的 WEB API 环境中。

<img src="largeimg.jpg">

现在,如果您想在图像加载后执行某些操作,则需要监听图像的“load”事件。

document.querySelector('img').addEventListener('load', imgLoadCallback);

现在一旦图片加载完毕,回调函数仍未执行,而是移入回调队列。回调函数在回调队列中等待,事件循环会检查同步代码,并等待直到调用栈为空。一旦调用堆栈为空,事件循环将在一个 事件循环滴答 中将第一个回调函数推入调用堆栈。那就是执行回调函数的时候。

但是,当存在 micro-tasks 时,这会发生变化,例如 Promises。当有承诺时,它被发送到微任务队列。微任务将始终优先于回调,它们可以并且将暂停回调直到它们被执行,事件循环将始终优先于微任务。

这就是 JavaScript 调用堆栈、事件循环、回调队列、微任务队列和 WEB API 环境的工作方式。

现在 运行 下面的代码,在 运行 尝试猜测结果之前。它将完全按照我上面写的:

//Synchronous Code - Always prioritized over async code
console.log('Asynchronous TEST start');

//It is a 0 Second Timer, But a timer is not a microtask
setTimeout(() => console.log('0 sec timer'), 0);

//Promise is a microtask
Promise.resolve('Resolved promise 1').then(res => console.log(res)); 

//2nd promise is a microtask too
Promise.resolve('Resolved promise 2').then(res => {
  for (let i = 0; i < 1000000000; i++) {} //very large loop
  console.log(res);
});

//Synchronous Code - Always prioritized over async code
console.log('Test end');

以上片段的剧透警告:

可以看到,定时器虽然是0秒的定时器,但是最后还是运行了,实际上并没有在0秒的时候执行。这是为什么?因为 Settimeout 使用回调,而 promises 是微任务,微任务优先级总是大于回调优先级