+= 异步函数

Async function with +=

let x = 0;

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

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

记录的 x 的值为 15。我的问题是:为什么 x 5 的值在第二个日志上?

如果 testx += 1 之后执行(因为它是一个异步函数)那么 x 的值在 [=16= 时是 1 ]被执行,所以x += await 5应该使x的值变成6.

你的声明 x += await 5 脱糖到

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

_temporary 值为 0,如果您在 await 期间更改 x(您的代码所做的)没关系,它会之后分配 5

是的,这有点棘手,实际发生的是两个加法运算都是并行发生的,所以运算就像:

承诺内:x += await 5 ==> x = x + await 5 ==> x = 0 + await 5 ==> 5

外部:x += 1 ==> x = x + 1 ==> x = 0 + 1 ==> 1

由于上述所有操作都是从左到右进行的,因此加法的第一部分可能会同时计算,并且由于在 5 之前有一个等待,加法可能会延迟一点。您可以通过在代码中放置断点来查看执行情况。

TL;DR: 因为+=之前读的是x,改了之后才写,由于await关键字在其第二个操作数(右侧)中。


async 在第一个 await 语句被调用时同步运行 运行。

因此,如果您删除 await,它的行为就像一个普通函数(除了它仍然 return 是一个 Promise)。

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

let x = 0;

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

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


第一个 await 停止同步 运行ning,即使它的参数是一个已经解析的承诺(或者在这里,根本不是一个承诺 - 这些将被转换为已解决的承诺await),所以下面将 return 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) 中的表达式,将其转换为已解决的承诺,并开始等待它。

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

x 现在是 1.

然后,在主脚本完成后,解释器返回到暂停的 test 函数,并继续计算该行,其中 await 不受影响,看起来像这样:

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

这段代码很难理解,因为它需要一些意外的异步来回跳转。让我们检查(接近)它实际上是如何执行的,我会在之后解释原因。我还更改了控制台日志以添加一个数字 - 使引用它们更容易并且还可以更好地显示记录的内容:

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) 将在 其余同步代码之后执行 。这是因为任何 Promise 都将在当前事件循环结束后得到解决。

所以,这解释了为什么我们首先记录 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

Async 和 Await 是 promises 的扩展。异步函数可以包含一个 await 表达式,该表达式暂停异步函数的执行并等待传递的 Promise 的解析,然后恢复异步函数的执行和 returns 已解析的值。请记住,await 关键字仅在异步函数内部有效。

即使您在调用测试函数后更改了 x 的值,x 的值仍然会保持为 0,因为异步函数已经创建了它的新实例。这意味着变量外部的所有变化都不会在它被调用后改变它内部的值。除非你把增量放在测试函数之上。