将临时变量分配给变量时异步函数的不同行为

Riw*_*iwa 1 javascript arrays variables promise async-await

为什么以下情况会出现不同的结果?第一个示例工作正常,返回一个包含三个元素的数组["qwe", "rty", "asd"]。第二个示例仅返回最后一个元素["asd"]。请解释一下它是如何工作的?为什么会发生这种行为?

在第一个示例中,通过中间变量进行工作awaitResult

class XXX {
  constructor() {
    this.storage = {1: ['qwe'], 2: ['rty'], 3: ['asd']}
  }

  async getValue(key) {
    return this.storage[key];
  }

  async logValues() {
    let keys = [1, 2, 3]
    let values = []

    // ----- First version -----

    await Promise.all(
      keys.map(
        async key => {
          let awaitResult = await this.getValue(key)
          values = values.concat(awaitResult)
        }
      )
    );

    console.log(values)
  }
}

let xxx = new XXX()
xxx.logValues()
Run Code Online (Sandbox Code Playgroud)

在第二个例子中,没有awaitResult.

class XXX {
  constructor() {
    this.storage = {1: ['qwe'], 2: ['rty'], 3: ['asd']}
  }

  async getValue(key) {
    return this.storage[key];
  }

  async logValues() {
    let keys = [1, 2, 3]
    let values = []

    // ----- Second version -----
   
    await Promise.all(
      keys.map(
        async key => values = values.concat(await this.getValue(key)),
      )
    );

    console.log(values)
  }
}

let xxx = new XXX()
xxx.logValues()
Run Code Online (Sandbox Code Playgroud)

VLA*_*LAZ 6

乔纳斯·威尔姆斯的回答绝对正确。我只是想对其进行一些澄清,因为有两个关键的事情需要理解:

异步函数实际上是部分同步的

我认为,这是最重要的事情。事情是这样的 - 异步函数知识 101:

  1. 他们稍后会执行。
  2. 他们返回一个 Promise。

但第一点实际上是错误的。异步函数将同步运行,直到遇到await关键字后跟 Promise,然后暂停,等待 Promise 解析并继续:

function getValue() {
  return 42;
}

async function notReallyAsync() {
  console.log("-- function start --");
  
  const result = getValue();
  
  console.log("-- function end --");
  
  return result;
}


console.log("- script start -");

notReallyAsync()
  .then(res => console.log(res));

console.log("- script end -");
Run Code Online (Sandbox Code Playgroud)

因此,在调用时将运行完成,因为其中notReallyAsync没有。await它仍然返回一个 Promise,该 Promise 只会被放入事件队列并在事件循环的下一次迭代中解决。

但是,如果它确实await,则该函数将在该点暂停,并且后的await任何代码将仅在 Promise 解决后运行:

function getAsyncValue() {
  return new Promise(resolve => resolve(42));
}

async function moreAsync() {
  console.log("-- function start --");
  
  const result = await getAsyncValue();
  
  console.log("-- function end --");
  
  return result;
}

console.log("- script start -");

moreAsync()
  .then(res => console.log(res));

console.log("- script end -");
Run Code Online (Sandbox Code Playgroud)

所以,这绝对是理解正在发生的事情的关键。第二部分实际上只是第一部分的结果

Promise 总是在当前代码运行后得到解决

是的,我之前提到过,但仍然 - 承诺解析是作为事件循环执行的一部分发生的。网上可能有更好的资源,但我写了一个简单的(我希望)概述它是如何工作的,作为我的答案的一部分。如果您在那里了解了事件循环的基本概念 - 很好,这就是您所需要的一切,基础知识。

本质上,现在运行的任何代码都在事件循环的当前执行范围内。任何承诺都将最早在下一次迭代中得到解决。如果有多个 Promise,那么您可能需要等待几次迭代。不管怎样,都是以后的事了。

那么,这一切如何应用在这里

为了更清楚起见,解释如下:之前的 代码将与其引用的任何内容的当前await值同步完成,而之后的代码将在下一个事件循环中发生: await

let awaitResult = await this.getValue(key)
values = values.concat(awaitResult) 
Run Code Online (Sandbox Code Playgroud)

意味着将首先等待该值,然后在解析时values获取该值并将awaitResult其连接到该值。如果我们表示按顺序发生的事情,您会得到如下结果:

let values = [];

//function 1: 
let key1 = 1;
let awaitResult1;
awaitResult1 = await this.getValue(key1); //pause function 1 wait until it's resolved

//function 2:
key2 = 2;
let awaitResult2;
awaitResult2 = await this.getValue(key2); //pause function 2 and wait until it's resolved

//function 3:
key3 = 3;
let awaitResult3;
awaitResult3 = await this.getValue(key3); //pause function 3 and wait until it's resolved

//...event loop completes...
//...next event loop starts 
//the Promise in function 1 is resolved, so the function is unpaused
awaitResult1 = ['qwe'];
values = values.concat(awaitResult1);

//...event loop completes...
//...next event loop starts 
//the Promise in function 2 is resolved, so the function is unpaused
awaitResult2 = ['rty'];
values = values.concat(awaitResult2);

//...event loop completes...
//...next event loop starts 
//the Promise in function 3 is resolved, so the function is unpaused
awaitResult3 = ['asd'];
values = values.concat(awaitResult3);
Run Code Online (Sandbox Code Playgroud)

因此,您将在一个数组中正确添加所有值。

然而,以下情况:

values = values.concat(await this.getValue(key))
Run Code Online (Sandbox Code Playgroud)

意味着首先 values将被获取,然后函数暂停以等待 的解析this.getValue(key)。由于values总是在对其进行任何修改之前获取,因此该值始终是一个空数组(起始值),因此这相当于以下代码:

let values = [];

//function 1:
values = [].concat(await this.getValue(1)); //pause function 1 and wait until it's resolved
//       ^^ what `values` is always equal during this loop

//function 2:
values = [].concat(await this.getValue(2)); //pause function 2 and wait until it's resolved
//       ^^ what `values` is always equal to at this point in time

//function 3:
values = [].concat(await this.getValue(3)); //pause function 3 and wait until it's resolved
//       ^^ what `values` is always equal to at this point in time

//...event loop completes...
//...next event loop starts 
//the Promise in function 1 is resolved, so the function is unpaused
values = [].concat(['qwe']);

//...event loop completes...
//...next event loop starts 
//the Promise in function 2 is resolved, so the function is unpaused
values = [].concat(['rty']);

//...event loop completes...
//...next event loop starts 
//the Promise in function 3 is resolved, so the function is unpaused
values = [].concat(['asd']);
Run Code Online (Sandbox Code Playgroud)

底线 - 的位置await 确实会影响代码的运行方式,从而影响其语义。

更好的写法

这是一个相当冗长的解释,但问题的实际根源是这段代码没有正确编写:

  1. 运行.map简单的循环操作是不好的做法。它应该用于执行映射操作 - 将数组的每个元素 1:1 转换为另一个数组。这里,.map只是一个循环。
  2. await Promise.all当有多个 Promise 等待时应该使用。
  3. values是异步操作之间的共享变量,它可能会遇到访问公共资源的所有异步代码的常见问题 - “脏”读取或写入可能会将资源从与实际状态不同的状态更改。这就是第二个中发生的情况每次写入都使用初始值 values而不是当前持有的代码版本。

适当地使用这些我们得到:

  1. 用于.map做出一系列 Promise。
  2. 用于await Promise.all等待以上所有问题都得到解决。
  3. 当 Promise 得到解决时,将结果values 同步合并。

class XXX {
  constructor() {
    this.storage = {1: ['qwe'], 2: ['rty'], 3: ['asd']}
  }

  async getValue(key) {
  console.log()
    return this.storage[key];
  }

  async logValues() {
  console.log("start")
    let keys = [1, 2, 3]

    let results = await Promise.all( //2. await all promises
      keys.map(key => this.getValue(key)) //1. convert to promises
    );
    
    let values = results.reduce((acc, result) => acc.concat(result), []); //3. reduce and concat the results
    console.log(values);
  }
}

let xxx = new XXX()
xxx.logValues()
Run Code Online (Sandbox Code Playgroud)

这也可以在运行时折叠到 Promise API 中Promise.all().then

class XXX {
  constructor() {
    this.storage = {1: ['qwe'], 2: ['rty'], 3: ['asd']}
  }

  async getValue(key) {
  console.log()
    return this.storage[key];
  }

  async logValues() {
  console.log("start")
    let keys = [1, 2, 3]

    let values = await Promise.all( //2. await all promises
      keys.map(key => this.getValue(key)) //1. convert to promises
    )
    .then(results => results.reduce((acc, result) => acc.concat(result), []));//3. reduce and concat the results
     
    console.log(values);
  }
}

let xxx = new XXX()
xxx.logValues()
Run Code Online (Sandbox Code Playgroud)