首页 > 解决方案 > 将临时分配给变量时异步函数的不同行为

问题描述

为什么在以下情况下会出现不同的结果?第一个示例工作正常,返回一个包含三个元素的数组["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()

在第二个示例中,没有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()

标签: javascriptarraysvariablespromiseasync-await

解决方案


Jonas Wilms的回答是绝对正确的。我只是想通过一些澄清来扩展它,因为有两个关键的事情需要理解:

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

我认为,这是最重要的。这是事情-异步函数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 -");

因此,notReallyAsync调用时将运行完成,因为其中没有await。它仍然返回一个 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 -");

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

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

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

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

那么,这一切如何适用于这里

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

let awaitResult = await this.getValue(key)
values = values.concat(awaitResult) 

表示将首先等待该值然后在解析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);

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

但是,以下内容:

values = values.concat(await this.getValue(key))

意味着首先 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']);

底线 - 的位置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()

这也可以在运行时折叠到 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()


推荐阅读