Tilldelningsoperator, map och promises. Vad är fel med den koden? Javascript


Jag höll på med några saker och stötte på ett problem som jag inte kan förstå. Jag förenklade koden för att få detta:

function somePromise() {
    return new Promise((resolve, reject) => {
        resolve(1);
    });
}

async function main() {
    let count = 0;
    const arr = [1, 2, 3, 4, 5];
    const promises = arr.map(async () => {
        count += await somePromise();
    })
    await Promise.all(promises);
    console.log(count);
}

main().then(() => console.log('Done'));

Vilket resultat förväntar du dig?

1
Klart

är inloggad.

När jag ändrar

count += await somePromise();

till

const nb = await somePromise();
count += nb;

Jag får

5
Klart

vad jag förväntade mig för första gången.

Kan du hjälpa mig att hitta vad som är fel? Jag förstår det inte.

Svar #1

När tolken stöter på await kommer den att pausa funktionen tills Promisens lösning. Även om Promisen löses omedelbart, kommer funktionen bara att återupptas under nästa mikrouppgift. Däremot itereras arrayen omedelbart, synkront. När du gör

const promises = arr.map(async () => {
    count += await somePromise();
})

Efter att arrayen har itererats igenom, men innan await-uttrycken har lösts, hämtas det "aktuella" värdet av count som används av += innan await har lösts - och värdet av count innan dess är 0. Så det ser ut för tolken som om det finns 5 separata uttryck:

count += await somePromise();
count += await somePromise();
count += await somePromise();
count += await somePromise();
count += await somePromise();

som löses till något som

const currentValueOfCount = count;
count = currentValueOfCount + await somePromise();
count = currentValueOfCount + await somePromise();
count = currentValueOfCount + await somePromise();
count = currentValueOfCount + await somePromise();
count = currentValueOfCount + await somePromise();

Så varje gång blir högersidan av = 0 + 1, så i slutet av loopen är count bara 1.

Om du är intresserad av var detta beskrivs i specifikationen, se på semantiken för tilldelningsoperatorer. Där += är en av AssignmentOperator-erna, är följande syntax:

VänsterSidUttryck Tilldelningsoperator Tilldelningsuttryck

gör :

  1. Låt lref vara resultatet av att utvärdera LeftHandSideExpression.
  2. Låt lval vara ? GetValue(lref).
  3. Låt rref vara resultatet av att utvärdera AssignmentExpression.
  4. Låt rval vara ? GetValue(rref).
  5. Låt op vara @ där AssignmentOperator är @=.
  6. Låt r vara resultatet av att tillämpa op på lval och rval som om man utvärderade uttrycket lval op rval.

Se hur lval hämtas omedelbart, innan högersidan av operatorn utvärderas. (Om lval hade hämtats efter att högersidan, AssignmentExpression, utvärderats, skulle resultaten ha varit 5, som du förväntar dig)

Här är ett exempel på detta beteende utan asynkrona operationer:

let num = 5;
const fn = () => {
  num += 3;
  return 0;
}
num += 2 + fn();
console.log(num);

Ovan hämtar num += 2 + fn(); omedelbart num som 5 för användning i +=, och sedan anropas fn(). Även om num tilldelas igen inuti fn, har det ingen effekt eftersom värdet av num redan har hämtats av den yttre +=.

Med din fungerande kod, när du gör

const nb = await somePromise();
count += nb;

Detta kommer att lägga värdet som somePromise löser i variabeln nb, och sedan kommer count += nb; att köras. Detta fungerar som förväntat eftersom det "aktuella" värdet av count som används för += hämtas efter att Promisen har lösts, så om en tidigare iteration hade tilldelat count på nytt kommer det att tas med korrekt i nästa iteration.