Ответ от Джонаса Уилмса абсолютно правильный. Я просто хочу остановиться на этом с некоторыми пояснениями, так как есть две ключевые вещи, которые нужно понимать:
Асинхронные функции на самом деле частично синхронны
Это, я думаюэто самая важная вещь. Вот в чем дело - знание асинхронных функций 101:
- Они будут выполняться позже .
- Они возвращают Обещание.
Но первый пункт на самом деле не так. Асинхронные функции будут запускаться синхронно до тех пор, пока они не встретят ключевое слово 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было довольно длинное объяснение, но фактический корень проблемы в том, что этот код написан неправильно:
- Выполнение
.map
для простой операции зацикливания - плохая практика. Его следует использовать для выполнения операции mapping - преобразования 1: 1 каждого элемента массива в другой массив. Здесь .map
является просто циклом. await Promise.all
следует использовать, когда есть несколько Обещаний, ожидающих. values
- это общая переменная между асинхронными операциями, которая может столкнуться с общими проблемами со всем асинхронным кодом, который обращается к общему ресурсу - «грязное» чтение или запись может изменить ресурс из состояния , отличного от , чемэто на самом деле. Это то, что происходит во второй версии кода, где каждая запись использует начальный values
вместо того, что он в настоящее время содержит.
Использование их соответствующим образоммы получаем:
- Используйте
.map
для создания массива Обещаний. - Используйте
await Promise.all
, чтобы подождать, пока все вышеперечисленное не будет решено. - Объединитьрезультаты в
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()