Разное поведение асинхронных функций при назначении временной переменной - PullRequest
0 голосов
/ 29 октября 2019

Почему другой результат в следующих случаях? Первый пример работает правильно, возвращает массив из трех элементов ["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()

Ответы [ 3 ]

3 голосов
/ 29 октября 2019

Ответ от Джонаса Уилмса абсолютно правильный. Я просто хочу остановиться на этом с некоторыми пояснениями, так как есть две ключевые вещи, которые нужно понимать:

Асинхронные функции на самом деле частично синхронны

Это, я думаюэто самая важная вещь. Вот в чем дело - знание асинхронных функций 101:

  1. Они будут выполняться позже .
  2. Они возвращают Обещание.

Но первый пункт на самом деле не так. Асинхронные функции будут запускаться синхронно до тех пор, пока они не встретят ключевое слово await, за которым следует Обещание, и , затем приостановка, дождитесь разрешения Обещания и продолжите:

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запускаться только после разрешения Обещания:

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 -");

Итак, это абсолютно ключ к пониманию происходящего. Вторая часть на самом деле является лишь следствием этой первой части

Обещания всегда разрешаются после текущий код выполняется

Да, я упоминал об этом раньше, но все же - обещаниеразрешение происходит как часть выполнения цикла событий. Возможно, есть лучшие ресурсы в Интернете, но я написал простую (надеюсь) схему того, как это работает , как часть моего ответа здесь . Если у вас есть основная идея цикла событий - хорошо, это все, что вам нужно, основы.

По сути, любой код, который запускает сейчас , находится в текущем выполнении цикла событий. Любое обещание будет выполнено в итерацию next как можно раньше. Если есть несколько Обещаний, вам может потребоваться подождать несколько итераций. В любом случае, это происходит позже .

Итак, как все это применимо здесь

Чтобы сделать это более понятным, вот объяснение: Код перед await будет завершено синхронно со значениями current всего, на что он ссылается, в то время как код после 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 влияет на то, как работает код, и, следовательно, на его семантику.

Лучший способ написать его

Thisбыло довольно длинное объяснение, но фактический корень проблемы в том, что этот код написан неправильно:

  1. Выполнение .map для простой операции зацикливания - плохая практика. Его следует использовать для выполнения операции mapping - преобразования 1: 1 каждого элемента массива в другой массив. Здесь .map является просто циклом.
  2. await Promise.all следует использовать, когда есть несколько Обещаний, ожидающих.
  3. values - это общая переменная между асинхронными операциями, которая может столкнуться с общими проблемами со всем асинхронным кодом, который обращается к общему ресурсу - «грязное» чтение или запись может изменить ресурс из состояния , отличного от , чемэто на самом деле. Это то, что происходит во второй версии кода, где каждая запись использует начальный values вместо того, что он в настоящее время содержит.

Использование их соответствующим образоммы получаем:

  1. Используйте .map для создания массива Обещаний.
  2. Используйте await Promise.all, чтобы подождать, пока все вышеперечисленное не будет решено.
  3. Объединитьрезультаты в 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()
2 голосов
/ 29 октября 2019

параллелизм. Или точнее: неатомарная модификация values.

Прежде всего, оценивается values.concat(...), тогда values - это пустой массив. Тогда все функции await. Затем все values = запускаются, объединяя ожидаемый элемент в пустой массив и присваивая этим массивам с одним значением values. Победит последнее разрешенное значение.

Исправить:

 await Promise.all(
  keys.map(
    async key => {
       const el = await this.getValue(key); // async operation
      values = values.concat(el); // atomic update
    }
  )
);
0 голосов
/ 29 октября 2019

Вы хотите изменить способ вычисления values, потому что вы можете сделать Promise.all полностью ответственным за это:

  async logValues() {
    const mapFn = async(key) => this.getValue(key);
    const values = await Promise.all(this.keys.map(mapFn));
    console.log(values)
    return values;
  }

Обратите внимание, что это работает, потому что мы используем одну строкуФункция стрелки: она автоматически возвращает результат оператора функции (который не , когда вы разбиваете тело функции стрелки на несколько строк с фигурными скобками).

Также я предполагаю keys на самом деле это не массив [1,2,3], потому что это было бы странно, но если вам нужно нужна последовательность чисел, и вы не хотите жестко кодировать этот массив, new Array(n).fill().map( (_,index) => console.log(index) ) где n - это число, которое должно сработать.

...