Javascript 异步函数的“等待”的正确心智模型:生成器的“产量”与“promise.then()”?

Posted

技术标签:

【中文标题】Javascript 异步函数的“等待”的正确心智模型:生成器的“产量”与“promise.then()”?【英文标题】:Correct mental model for a Javascript async function's 'await': generator's 'yield' vs. 'promise.then()'? 【发布时间】:2019-11-15 10:13:02 【问题描述】:

生成器的 yield 与 promise.then() 哪个是理解“等待”的更*正确的心理模型?

属性比较,通过使用调试器单步执行下面的 sn-p 来推断:

等待:

    await 不会暂停/暂停正在运行的异步函数的执行。 (运行中的异步函数“运行到完成”,当解释器遇到第一个等待时返回一个待处理的承诺。然后立即从调用堆栈中删除。)

    await 等待承诺解决。

    await expression 将函数的其余代码封装在一个微任务中。

发电机产量:

    yield 暂停正在运行的函数的执行。生成器函数不会“运行完成”。 yield promise 确实确保 promise 在执行剩余代码之前已解决。 yield 不会包装或创建微任务。

promise.then(回调):

    不暂停正在运行的函数的执行。 在执行回调之前等待承诺解决。 创建一个微任务(回调)

//promise returning function
function foo(whoCalled) 
   let p = new Promise(function(resolve, reject)  
     setTimeout( () => 
       console.log('resolving from setTimeout - called by: ' + whoCalled)
       resolve('resolve value') , .1)
   )
   return p


//async await
async function asyncFunc() 
  await foo('async function')
  //rest of running function’s code…
  console.log('async function howdy')


//generator yield:
function* gen() 
   yield foo('generator function')
   //rest of running function’s code…
   console.log('generator function howdy')


//promise.then():
function thenFunc() 
   let r = foo('promise.then function').then(() => 
       //rest of running function’s code…
       console.log('promise.then() howdy')
   )
   return r


//main
function main() 

  //async await
  var a = asyncFunc() 
  console.log(a) //logs Promise  <pending> 
                 //the rest of the code following await foo() runs as a microtask runs once foo() resolves. The call stack was cleared.

  //generator
   var g = gen()
   console.log(g) // logs Object [Generator] 
   var p = g.next().value
   console.log(p) //logs Promise  <pending> 
   g.next()       //the rest of the code following yield running gen function's code runs. call stack was not cleared.

   //promise.then()
   var x = thenFunc()
   console.log(x) //logs Promise  <pending> 
                   //the then(callback) microtask runs once foo() resolves. The call stack was cleared

main()
console.log('main is off the call stack - launch/startup macrotask completing. Event loop entering timer phase.')

除了这个比较之外,await 在幕后所做的准确心智模型是什么?

等待最新的 ECMAScript 规范以供参考: https://www.ecma-international.org/ecma-262/10.0/index.html#await

在 V8 源代码中等待:https://github.com/v8/v8/blob/4b9b23521e6fd42373ebbcb20ebe03bf445494f9/src/builtins/builtins-async-function-gen.cc#L252

【问题讨论】:

await 就像.then() await 不会暂停/暂停执行。”和“当解释器遇到第一个等待时。然后立即从调用堆栈中删除。 i>”对我来说确实矛盾。那段话是什么意思? babeljs.io/repl 有助于解决这个问题。复制粘贴你的代码,看看它是如何模拟 await 关键字的。 @Bergi 第一个引号 = 描述函数代码中的暂停以供以后恢复(未运行到完成),而第二个引号 = 描述将函数代码的其余部分包装在微任务中,打到结尾花括号并返回(运行至完成)。 @ToddChaffee 好提示。下面的说法正确吗? Transpiling 可以生成功能上等效的 vanilla javascript。虽然这可能有助于推断“幕后”发生的事情,但它可能会产生误导? (例如:转译的异步等待可能不会映射到 ECMAScript 异步等待规范) 【参考方案1】:

Promise 和 yield 并不是最容易掌握的,尤其是当您不知道它们在后台是如何工作的时候。所以让我们从基础开始。首先要了解的是 Javascript 是单线程的,这意味着它只能同时做一件事。您仍然能够“一次”执行多项操作的方式是因为 javascript 有一个称为事件循环的东西。

事件循环基本上是这样的:

while(queue.waitForTasks()) 
   queue.performNextTask();

事件循环的作用是检查是否有新的“任务”供 Javascript 运行。如果有任务。然后它被执行,直到没有更多的任务要执行。它会等待它的新任务。这些任务存储在称为队列的东西中。

承诺,异步/等待

现在我们了解了 Javascript 如何处理不同的任务。它如何与 Promise 和 async/await 一起工作? promise 只不过是一个任务,或者在 Javascript 的情况下包含一个任务,它将被添加到队列中,并在所有任务执行之前执行一次。 .then() 是一种为你的 promise 提供回调的方法,一旦你的 resolve 回调被调用,它就会被执行。

await [something] 关键字告诉 Javascript,嘿,将下一个 [something] 放在队列的末尾,一旦 [something] 有结果要回复我。

具有async 关键字的函数基本上是在告诉Javascript:“这个函数是一个promise,但要立即执行”。

使用两个不同的异步函数 A 和 B 最容易掌握/演示异步函数的流程,如下所示:

const A = async () => 
    console.log(A: Start);
    for (var i = 0; i < 3; i++) 
        await (async () => console.log('A: ' + i));
    
    console.log('A: Done');

const B = async () 
    console.log(B: Start);
    for (var i = 0; i < 3; i++) 
        await (async () => console.log('B: ' + i));
        await (async () => /* A task without output */);
    
    console.log('B: Done');

当你像这样用 await 调用你的函数时:

console.log('Executing A');
await A();
console.log('Executing B');
await B();

这会导致:

Executing A
A: Start
A: 0
A: 1
A: 2
A: Done
Executing B
B: Start
B: 0
B: 1
B: 2
B: Done

并运行:

console.log('Executing A');
A();
console.log('Executing B');
B();

会导致:

Executing A
A: Start       Note: still gets ran before Executing B
Executing B
B: Start
A: 0
B: 0
A: 1
A: 2           Note: A: 2 first because another task in B was put in the queue
A: Done
B: 1
B: 2
B: Done

了解这一点可能有助于更好地了解应用程序的流程。

产量

yield 关键字在某种意义上类似于await,因为“外力”控制着它继续执行函数的流程。这种情况下不是promise任务的完成,而是generator.next()函数

【讨论】:

Promise 不是任务,它不会被放入事件队列中。这是执行then 回调的任务,该回调当承诺履行时被放入队列。 好吧,也许我应该稍微改变一下措辞,因为它不是很准确。你可能已经注意到了。不过我稍后会这样做,这已经花了一些时间来写哈哈 @SoftwarePerson “承诺只不过是一项任务。”不正确。可能会说“承诺只不过是一个代表最终返回/解决值的对象”。 “await [something] 关键字告诉 Javascript,嘿,将下一个 [something] 放在队列的末尾,一旦 [something] 有结果要给我,就给我。”不正确。 [something] 立即同步执行。它是封装在微任务中的后续函数代码。 @SoftwarePerson “具有 async 关键字的函数基本上是在告诉 Javascript:‘这个函数是一个承诺,但要立即执行’。”不正确。 async 关键字被解释为“这个函数返回一个 promise,并且 await 可以在 func 主体中使用”。 代码不会生成该结果。通过await (async () =&gt; console.log('A: ' + i)),您正在等待匿名函数的定义,这确实是一个非thenable 对象。您需要执行匿名函数:await ((async () =&gt; console.log('A: ' + i))())【参考方案2】:

这不是一个或另一个。实际上是他们两个在一起:async/await = yield + then + 一个跑步者。

async function 确实被 await 关键字挂起,就像生成器 function* 确实被 yield 关键字挂起一样。在控制流语句中间停止和恢复执行的机制完全相同。

不同的是如何驱动这些延续,以及函数返回什么。生成器函数在调用时会创建一个生成器对象,您必须从外部显式调用next() 方法才能运行yield 的代码yield。另一方面,异步函数创建一个 Promise,并自行管理执行。它不会等待外部next() 调用,而是尽快运行每个异步步骤。它不是从那些next() 调用返回产生的值,而是将Promise.resolve() 等待的值传递给一个promise,并调用它的then 方法,将延续作为回调传递。当到达 return 时,它不会向调用者发出“迭代结束”信号,而是使用返回值解析最初返回的 Promise。

【讨论】:

您好,Bergi,您能多谈谈“尽快”的性质吗? Async/await 如此成功地模仿了同步代码,这是一个真正的危险,人们误解了带有 await(s) 的 asyncFunction 的非阻塞性质。 @Roamer-1888 我已经明确表示执行仍然是异步的。 @Bergi 谢谢。 v8 await source code 是您用来确定这一点的那种东西吗?或者ECMASpec抽象操作比较async functiongenerator。另外,不确定这里的“跑步者”是什么意思? @Bergi 关于:“......并调用其then 方法,将延续作为回调传递。”我似乎找不到在 v8 await src 代码中实现 .then(continuation code callback) 的位置。 @AnonEq 我从关于行为的规范和关于优化的 V8 博客中学习。我不读代码。 “运行器”是指驱动(运行)代码的实体的抽象概念。在转译的代码中,例如是一个函数,负责处理等待的承诺并通过安装 then 回调来保持事情的顺利进行。【参考方案3】:

我不知道这里正确心智模型的答案,但我真的很想知道。

但我觉得这很有趣

“You Don't Know JS”的作者 Kyle Simpson 附和了 await 在 r/Javascript reddit 上的工作原理 - source:

“这完全不正确。生成器不会运行到完成,并且 async-await 的大多数引擎实现实际上将它们视为 发电机。当遇到yield时,生成器在本地 停顿了……字面意思。 Await 使用相同的方法。”

“不,这都是不正确的废话。大多数引擎都将异步等待 就像一个生成器,它肯定会在局部暂停。 将 promise.then() 包装在后续代码周围将是实现 await 的最幼稚和低效的方法之一。 即使一个 引擎做到了(大多数人没有)这并不意味着这是正确的 精神的。模型。像 yield 这样的局部停顿是正确的心智模型。”

但当我亲自查看 ECMA 脚本规范并使用 vscode nodejs 调试器浏览代码时,await 似乎更类似于 .then()

【讨论】:

以上是关于Javascript 异步函数的“等待”的正确心智模型:生成器的“产量”与“promise.then()”?的主要内容,如果未能解决你的问题,请参考以下文章

等待异步函数在 dart 中无法正确执行

JavaScript在if语句中等待异步函数[重复]

如何在同步函数中等待 JavaScript 中的异步调用?

如何在 Javascript 中使用异步等待函数对象?

如果javascript“异步”函数的主体中没有“等待”,它会以任何不同的方式执行吗? [复制]

异步 - 等待 JavaScript:无法从错误对象中捕获错误详细信息 [重复]