Correct mental model for a Javascript async function's 'await': generator's 'yield' vs. 'promise.then()'?

早过忘川 提交于 2021-02-18 07:43:12

问题


Which of a generator's yield vs. promise.then() is a more* correct mental model for understanding 'await'?

Property comparison, inferred by stepping through the snippet below with a debugger:

await:

  1. await does not pause/suspend the running async function’s execution. (The running async function ‘runs to completion’, returning a pending promise when the interpreter hits the 1st await. It’s then immediately removed from the call stack.)

  2. await waits for the promise to settle.

  3. await expression wraps the rest of a function's code in a microtask.

generator-yield:

  1. yield pauses the running function’s execution. generator functions do not ‘run to completion’.
  2. yield promise does ensure promise has settled prior to executing remaining code.
  3. yield does not wrap or create a microtask.

promise.then(callback):

  1. does not pause the running function’s execution.
  2. waits for promise to settle before executing callback.
  3. creates a microtask (callback)

//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.')

And, going beyond this comparison, what is the accurate mental model of what await does under the hood?

await in latest ECMAScript spec for reference: https://www.ecma-international.org/ecma-262/10.0/index.html#await

await in V8 source code: https://github.com/v8/v8/blob/4b9b23521e6fd42373ebbcb20ebe03bf445494f9/src/builtins/builtins-async-function-gen.cc#L252


回答1:


It's not one or the other. Actually it's both of them together: async/await = yield + then + a runner.

An async function does get suspended by the await keyword just like a generator function* does get suspended by the yield keyword. The mechanism of how the execution gets stopped and resumed in the middle of control flow statements is exactly the same.

What differs is how these continuations are driven, and what the functions return. A generator function creates a generator object when called, and you have to explicitly invoke the next() method from outside to run the code yield by yield. An async function on the other hand creates a promise, and manages the execution by itself. It doesn't wait for external next() calls but runs each asynchronous step as soon as possible. Instead of returning the yielded values from those next() calls, it does Promise.resolve() the awaited values to a promise, and calls its then method passing the continuation as the callbacks. Instead of signalling an "end of iteration" to the caller when reaching a return, it resolves the originally returned promise with the return value.




回答2:


Promises and yield are not the easiest to grasp, especially not when you don't know how they work under the hood. So let's start with the basics. The first thing to understand is that Javascript is single threaded, which means that it can only do one thing at the same time. The way you are still able to multiple things at 'once' is because javascript has a thing called an event loop.

The event loop is basically looks something like this:

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

What the event loop does is check if there are new 'tasks' for Javascript to run. If there is a task. then it gets executed until there are no more tasks left to execute. And it will wait for its new task. These tasks are stored in something that is called a queue.

Promises, Async/Await

Now we understand how Javascript processes the different tasks. How does it work with promises, and async/await? A promise is nothing more than a task, or in the case of Javascript something that holds a task, that will be added to the queue and executed once all tasks before it have been executed. The .then() is a way of providing a callback to your promise that gets executed once your resolve callback is called.

the await [something] keyword tells Javascript, hey put the next [something] on the end of your queue, and get back to me once that [something] has a result to give.

A function that has the async keyword is basically telling Javascript: 'This function is a promise, but execute it immediately'.

The flow of a async function is easiest to grasp/demonstrate with two different async functions A and B like this:

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');
}

When you are calling your functions with await like this:

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

it would result in:

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

and running:

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

would result in:

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

Understanding this might help to better understand the flow of your application.

yield

The yield keyword is similar to await in the sense that an 'outside force' controls when it continues the flow of the function. In this case not the completion of the promise task, but the generator.next() function




回答3:


I don't know the answer to the correct mental model here, though I would really like to know.

But I found this interesting

Kyle Simpson author of 'You Don't Know JS' chimed in on how await works on r/Javascript reddit - source:

"This is entirely incorrect. Generators do not run-to-completion, and most engine implementations of async-await actually treat them like generators. When a yield is encountered, the generator is locally paused... literally. Await uses the same approach."

"No, this is all incorrect nonsense. Most engines treat async-await like a generator, which definitely does localy pause at a yield. Wrapping promise.then() around subsequent code would be one of the most naive and inefficient ways to implement await. Even if an engine did that (most don't) that doesn't mean that's the proper mental. model. Local pausing like yield is the proper mental model."

But when I personally look at the ECMA Script spec myself and walk though code with the vscode nodejs debugger, await seems much more analogous to .then()



来源:https://stackoverflow.com/questions/56891838/correct-mental-model-for-a-javascript-async-functions-await-generators-yie

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!