Javascript promises 是否会阻塞堆栈

Do Javascript promises block the stack

当使用 Javascript promise 时,事件循环会被阻塞吗?

我的理解是,使用 await 和 async 会使堆栈停止,直到操作完成。它是通过阻塞堆栈来实现的,还是类似于回调并将过程传递给某种 API?

一个await只阻塞当前的async function,事件循环继续正常运行。当 promise 结束时,函数体的执行从它停止的地方恢复。

每个 async/await 都可以转换为等效的 .then(…)-回调程序,并且从并发的角度来看就像那样工作。因此,当一个承诺被 await 编辑时,其他事件可能会触发并且任意其他代码可能 运行.

When using Javascript promises, does the event loop get blocked?

没有。 Promises 只是一个事件通知系统。它们本身不是操作。他们只是通过调用适当的 .then().catch() 处理程序来响应被解决或拒绝,如果链接到其他承诺,他们可以延迟调用这些处理程序,直到他们链接到的承诺也 resolve/reject .因此,单个 promise 不会阻止任何东西,当然也不会阻止事件循环。

My understanding is that using await & async, makes the stack stop until the operation has completed. Does it do this by blocking the stack or does it act similar to a callback and pass of the process to an API of sorts?

await 只是语法糖,它用更简单的语法替换了 .then() 处理程序。但是,在幕后操作是相同的。 await 之后的代码基本上放在一个不可见的 .then() 处理程序中,并且没有事件循环的阻塞,就像 .then() 处理程序没有阻塞一样。


注意解决以下评论之一:

现在,如果您要构建的代码通过不断解决承诺(在此处某些评论中提出的某种无限循环中)来淹没事件循环,那么事件循环将一遍又一遍地处理那些不断解决的承诺来自微任务队列的承诺,永远不会有机会处理在事件循环(其他类型的事件)中等待的宏任务。事件循环仍然是 运行 并且仍在处理微任务,但是如果你不断地向其中填充新的微任务(已解决的承诺),那么它可能永远不会到达宏任务。关于是否将其称为“阻塞事件循环”似乎存在一些争论。这只是一个术语问题——更重要的是实际发生了什么。在这个无限循环的例子中,一遍又一遍地解决一个新的承诺,事件循环将继续处理那些已解决的承诺,而事件队列中的其他事件将不会得到处理,因为它们永远不会排到队列的最前面来获取它们的转动。这通常被称为“饥饿”而不是“阻塞”,但关键是,如果您不断地将新的微任务放入队列中,宏任务可能得不到服务。

在 Javascript 中应避免无限循环不断解决新承诺的概念。它可能会导致其他事件无法获得服务。

正如上面提到的其他...Promises 就像一个事件通知系统,async/awaitthen() 相同。但是,要非常小心,您可以通过执行阻塞操作来 "block" 事件循环。看看下面的代码:

function blocking_operation_inside_promise(){
    return new Promise ( (res, rej) => {
        while( true ) console.log(' loop inside promise ')
        res();
    })
}

async function init(){
    let await_forever = await blocking_operation_inside_promise()
}

init()
console.log('END')

永远不会打印 END 日志。 JS 是单线程的,那个线程现在很忙。你可以说整个事情是 "blocked" 通过阻塞操作。在这种特殊情况下,事件循环本身没有被阻塞,但它不会将事件传递给您的应用程序,因为主线程很忙。

JS/Node 可以是一种非常有用的编程语言,在使用非阻塞操作(如网络操作)时非常高效。但不要用它来执行非常激烈的 CPU 算法。如果您在浏览器端,请考虑使用 Web Workers,如果您在服务器端,请考虑使用工作线程、子进程或微服务架构。

Do Javascript promises block the stack

不,不是堆栈。在 Promise 的回调开始执行之前,当前作业将 运行 完成。

When using Javascript promises, does the event loop get blocked?

是的。

不同的环境有不同的event-loop处理模型,所以我说的是the one in browsers,不过nodejs的模型虽然简单了点,但其实暴露的是一样的行为。

在浏览器中,Promises 的回调(PromiseReactionJob 在 ES 术语中)实际上是在所谓的 microtask.
中执行的 microtaskgets queued in the special microtask-queue 的特殊任务。 这个 microtask-queue 在单个 event-loop iteration in what is called a microtask-checkpoint, and every time the JS call stack is empty 期间被访问了不同的时间,例如在完成主要任务之后,在执行像调整大小这样的渲染事件之后,在每个 animation-frame回调等

这些 microtask-checkpoints 是 event-loop 的一部分,并且会在 运行 就像任何其他任务一样。
然而,关于这些的更多信息是,从 microtask-checkpoint 调度的 微任务 将由相同的 [ 执行=84=].

这意味着使用 Promise 的简单事实不会让您的代码让 event-loop 呼吸,就像 setTimeout() 计划任务可以做的那样,即使 js 堆栈已经清空并且在回调被调用之前之前的任务已经完全执行,你仍然可以很好地完全锁定 event-loop,不允许它处理任何其他任务甚至更新渲染:

const log = document.getElementById( "log" );
let now = performance.now();
let i = 0;

const promLoop = () => {
  // only the final result will get painted
  // because the event-loop can never reach the "update the rendering steps"
  log.textContent = i++;
  if( performance.now() - now < 5000 ) {
    // this doesn't let the event-loop loop
    return Promise.resolve().then( promLoop );
  }
  else { i = 0; }
};

const taskLoop = () => {
  log.textContent = i++;
  if( performance.now() - now < 5000 ) {
    // this does let the event-loop loop
    postTask( taskLoop );
  }
  else { i = 0; }
};

document.getElementById( "prom-btn" ).onclick = start( promLoop );
document.getElementById( "task-btn" ).onclick = start( taskLoop );

function start( fn ) {
  return (evt) => {
    i = 0;
    now = performance.now();
    fn();
  };
}

// Posts a "macro-task".
// We could use setTimeout, but this method gets throttled
// to 4ms after 5 recursive calls.
// So instead we use either the incoming postTask API
// or the MesageChannel API which are not affected
// by this limitation
function postTask( task ) {
  // Available in Chrome 86+ under the 'Experimental Web Platforms' flag
  if( window.scheduler ) {
    return scheduler.postTask( task, { priority: "user-blocking" } );
  }
  else {
    const channel = postTask.channel ||= new MessageChannel();
    channel.port1
      .addEventListener( "message", () => task(), { once: true } );
    channel.port2.postMessage( "" );
    channel.port1.start();
  }
}
<button id="prom-btn">use promises</button>
<button id="task-btn">use postTask</button>
<pre id="log"></pre>

所以请注意,使用 Promise 对让 event-loop 实际上循环没有任何帮助。

我们经常看到代码使用批处理模式来不阻止完全失败的 UI 目标,因为它假设 Promises 会让 event-loop 循环。为此,请继续使用 setTimeout() 作为安排任务的方式,或者如果您在不久的将来,请使用 postTask API

My understanding is that using await & async, makes the stack stop until the operation has completed.

有点...当await输入一个值时,它会将函数执行的剩余部分添加到附加到等待的 Promise 的回调中(这可以是一个新的 Promise 解析 non-Promise值)。
所以此时栈确实被清空了,但是事件循环在这里完全没有被阻塞,相反它被释放出来执行其他任何事情直到Promise resolve。

这意味着您可以很好地等待一个永远不会解决的承诺,并且仍然让您的浏览器正常运行。

async function fn() {

  console.log( "will wait a bit" );
  const prom = await new Promise( (res, rej) => {} );
  console.log( "done waiting" );
  
}
fn();

onmousemove = () => console.log( "still alive" );
move your mouse to check if the page is locked