异步函数与 += 的使用方法

71

let x = 0;

async function test() {
    x += await 5;
    console.log('x :', x);
}

test();
x += 1;
console.log('x :', x);

记录的 x 值为 15。我的问题是:为什么第二次记录时 x 的值是 5
如果在执行 x += 1 后(因为它是一个异步函数)执行了 test,那么在执行 test 时,x 的值为 1,所以 x += await 5 应该将 x 的值变成 6

1
你必须知道 await (x += 5)x += await 5 的区别。 - Singhi John
5个回答

66

TL;DR: 因为+=在其第二操作数(右侧)中有await关键字,导致它在更改x之前读取了它,但在更改之后才写入。


async函数在调用时同步运行,直到第一个await语句。

因此,如果删除await,它的行为就像普通函数一样(唯一的例外是它仍然返回Promise)。

在这种情况下,您将在控制台中获得5(来自函数)和6(来自主脚本):

let x = 0;

async function test() {
  x += 5;
  console.log('x :', x);
}

test();
x += 1;
console.log('x :', x);


await 关键字会使同步代码挂起,即使它的参数已经是一个已解决的 promise(或者像此处一样,根本不是一个 promise - 这些将被 await 转换为已解决的 promise)。因此,以下代码会如你所预期的返回 1(来自主脚本)和 6(来自函数):

let x = 0;

async function test() {
  // Enter asynchrony
  await 0;

  x += 5;
  console.log('x :', x);
}

test();
x += 1;
console.log('x :', x);


然而,你的情况稍微有些复杂。

你在一个使用 += 的表达式中放置了 await

你可能知道,在JS中,x += y 等同于 x = (x + y) (除非 x 是一个具有副作用的表达式,但这里不是这种情况)。我将使用后者形式以使其更易于理解:

let x = 0;

async function test() {
  x = (x + await 5);
  console.log('x :', x);
}

test();
x += 1;
console.log('x :', x);

当解释器执行到这行代码时...

x = (x + await 5);

...它开始评估它,替换x,因此它变成了...

x = (0 + await 5);

接着,它会评估await内部的表达式(5),将其转换为已解决的 Promise,并开始等待。

函数调用后的代码开始运行,并修改了x的值(从0变为1),然后将其记录下来。

x现在是1

然后,在主脚本完成之后,解释器返回到暂停的test函数,并继续评估该行,这样看起来:

x = (0 + 5);

而且,由于x的值已经被替换,它仍然是0

最后,解释器进行加法运算,将5存储到x并记录它。

您可以通过在对象属性getter/setter内部(在本例中为y.z,它反映了x的值)进行记录来检查此行为:

let x = 0;
const y = {
  get z() {
    console.log('get x :', x);
    console.log(new Error().stack.replace('Error', 'Stacktrace')); //Log stacktrace using an Error object
    return x;
  },
  set z(value) {
    console.log('set x =', value);
    console.log(new Error().stack.replace('Error', 'Stacktrace')); //Log stacktrace using an Error object
    x = value;
  }
};

async function test() {
  console.log('inside async function');
  y.z += await 5;
  console.log('x :', x);
}

test();
console.log('main script');
y.z += 1;
console.log('x :', x);
console.log('end of main script')

/* Output:

inside async function
get x : 0 <-------------- async fn reads
Stacktrace
    at Object.get z [as z] (https://stacksnippets.net/js:19:17)
    at test (https://stacksnippets.net/js:31:3) <-- async fn is synchronous here
    at https://stacksnippets.net/js:35:1 <--------- (main script is still in the stack)

main script
get x : 0
Stacktrace
    at Object.get z [as z] (https://stacksnippets.net/js:19:17)
    at https://stacksnippets.net/js:37:1
set x = 1
Stacktrace
    at Object.set z [as z] (https://stacksnippets.net/js:24:17)
    at https://stacksnippets.net/js:37:5
x : 1
end of main script

set x = 5 <-------------- async fn writes
Stacktrace
    at Object.set z [as z] (https://stacksnippets.net/js:24:17)
    at test (https://stacksnippets.net/js:31:7) <-- async fn is asynchronous (main script is no longer in the stack)
x : 5 <------------------ async fn logs

*/
/* Just to make console fill the available space */
.as-console-wrapper {
  max-height: 100% !important;
}


@MaximPro 我认为我们在用不同的措辞表达同样的意思。你为什么认为我是错的?是我的回答中哪一部分让你不同意?我乐于接受建设性的批评。 - FZs
@FZs 也许你是对的,无论如何,在当前时间段内最好的决定是规范tc39。这里有一个链接 https://tc39.es/ecma262/#sec-async-function-definitions-runtime-semantics-evaluation。 在暂停异步代码之前,将完成前两个步骤,然后在第三步中将发生暂停异步代码。我想你明白我的意思,以及为什么 x += await 5 不等于 x = x + await 5。 P.S 当然,我想回答这个问题,但不幸的是我没有足够的时间。 - MaximPro
1
@MaximPro “按照你的逻辑,在await中使用console.log将被延迟执行。”也许我没有表达清楚,但我的意思并非如此。在相关代码中这并不重要,因为表达式5没有副作用。但是,由于5(或console.log(2))位于(操作数内),它必须首先被评估,然后该表达式的结果将被等待,因此被延迟执行。同样,包含await的表达式(例如console.log(await 2)甚至await 0, console.log(2)被延迟执行。我很快会编辑我的答案来说明这一点。 - FZs
@FZs 现在,我受欢迎了,这听起来让人有信心你理解我在说什么。好吧,如果你能编辑你的帖子并考虑我们的小谈话,那将是很好的。 - MaximPro
1
做得好,@FZs!现在看起来更好了。 - MaximPro
显示剩余6条评论

12

你的语句 x += await 5 被展开为

const _temp = x;
const _gain = await 5;
x = _temp + _gain;

_temporary的值为0,如果你在await期间(你的代码会这样)更改x的值也没关系,之后它会被赋值为5


9

这段代码相当复杂,因为它会进行一些意外的异步跳转。让我们接近地查看它实际上将如何执行,并在此之后解释为什么。我还更改了控制台日志以添加一个数字-这样引用它们更容易,也更好地显示了记录内容:

let x = 0;                        // 1 declaring and assigning x

async function test() {           // 2 function declaration
    x += await 5;                 // 4/7 assigning x
    console.log('x1 :', x);       // 8 printing
}

test();                           // 3 invoking the function
x += 1;                           // 5 assigning x
console.log('x2 :', x);           // 6 printing

因此,代码肯定不会直接进行。我们还有一个奇怪的4/7的东西。这就是整个问题所在。

首先,让我们澄清一下-异步函数实际上并不是严格异步的。只有在使用await关键字时它们才会暂停执行并稍后恢复;否则,它们按顺序同步地逐个表达式执行:

async function foo() {
  console.log("--one");
  console.log("--two");
}

console.log("start");
foo();
console.log("end");

async function foo() {
  console.log("--one");
  await 0; //just satisfy await with an expression
  console.log("--two");
}

console.log("start");
foo();
console.log("end");

那么,我们首先需要知道使用await会使函数的剩余部分延迟执行。在给定的例子中,这意味着console.log('x1 :', x)将在其余同步代码之后执行。这是因为任何Promises都将在当前事件循环结束后解决。
这就解释了为什么我们首先得到记录x2 : 1,为什么第二个记录是x2 : 5,但不是后一个值为5。从逻辑上讲,x += await 5应该是5...但这里是await关键字的第二个问题 - 它将暂停函数的执行,但它之前的所有内容已经运行。 x += await 5实际上将以以下方式处理:
  1. 获取x的值。在执行时,这是0
  2. await下一个表达式,即5。所以,函数现在暂停并稍后恢复。
  3. 恢复函数。表达式解析为5
  4. 添加来自1.和2/3的表达式的值:0 + 5
  5. 将4的值分配给x
因此,函数在读取x0后暂停,并在其已更改时恢复,但它不会重新读取x的值。
如果我们将await展开为其等效的Promise,则为:

let x = 0;                        // 1 declaring and assigning x

async function test() {           // 2 function declaration
    const temp = x;               // 4 value read of x
    await 0; //fake await to pause for demo
    return new Promise((resolve) => {
      x = temp + 5;               // 7 assign to x
      console.log('x1 :', x);     // 8 printing
      resolve();
    });
}

test();                           // 3 invoking the function
x += 1;                           // 5 assigning x
console.log('x2 :', x);           // 6 printing


3
这有点棘手,实际上两个加法操作是同时进行的,所以操作将会如下:
在promise内部: x += await 5 ==> x = x + await 5 ==> x = 0 + await 5 ==> 5 外部:x += 1 ==> x = x + 1 ==> x = 0 + 1 ==> 1 由于所有上述操作是从左至右进行的,因此加法的第一部分可能会在同一时间计算,而由于5前有await,所以该加法可能会稍微延迟一些。您可以通过在代码中设置断点来查看执行情况。

0
Async和Await是Promise的扩展。异步函数可以包含等待表达式(await expression),它会暂停异步函数的执行并等待传递的Promise解决,然后恢复异步函数的执行并返回已解决的值。请记住,await关键字只在异步函数内部有效。
即使在调用test函数后更改了x的值,x的值仍将保持为0,因为异步函数已经创建了它的新实例。这意味着在变量外部进行的所有更改都不会在调用后改变其内部的值。除非您将增量放在test函数之上。

1
“意思是说,变量外部的所有更改不会影响其内部在调用后的值。”这并不正确。异步函数在执行期间确实会接收到变量更改。只需尝试此操作:let x="没有接收到更改"; (async()=>{await 'Nothing'; console.log(x); await new Promise(resolve=>setTimeout(resolve,2000)); console.log(x)})(); x='同步接收到更改'; setTimeout(()=>{x='接收到更改'},1000) 它输出 同步接收到更改接收到更改 - FZs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接