promise.all и Event loop

93
06 февраля 2022, 06:20

Насколько я понял принцип работы Event Loop, при выполнении кода промисы попадают в очередь Microtask queue и затем выполняются поочередно, потому что java script работает в один поток.

Вместе с тем, метод Promise.all() выполняет промисы параллельно. Вот пример кода:

var p1 = new Promise((resolve, reject) => { 
  setTimeout(resolve, 1000, "one"); 
}); 
var p2 = new Promise((resolve, reject) => { 
  setTimeout(resolve, 1000, "two"); 
});
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "three");
});
var p4 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "four");
});

Promise.all([p1, p2, p3, p4]).then(value => { 
  console.log(value);
});

Результат выполнения скрипта через 1 секунду: [ 'one', 'two', 'three', 'four' ] То есть выполнение и правда выглядит параллельно.

Вместо setTimeout() я решил дать промисам долгую задачу и измерить время выполнения каждого промиса.

var p1 = new Promise((resolve, reject) => { 
  let start = new Date().getTime();
  let i = 1;
  while (i < 1e10) {
    i++;
  }
  let end = new Date().getTime();
  console.log(`p1: ${(end - start)/1000}s`);
  resolve(i);
}); 
var p2 = new Promise((resolve, reject) => { 
  let start = new Date().getTime();
  let i = 1;
    while (i < 1e10) {
      i++;
    }
    let end = new Date().getTime();
    console.log(`p2: ${(end - start)/1000}s`);
    resolve(i);
});
var p3 = new Promise((resolve, reject) => {
  let start = new Date().getTime();
  let i = 1;
    while (i < 1e10) {
      i++;
    }
    let end = new Date().getTime();
    console.log(`p3: ${(end - start)/1000}s`);
    resolve(i);
});
var p4 = new Promise((resolve, reject) => {
  let start = new Date().getTime();
  let i = 1;
    while (i < 1e10) {
      i++;
    }
    let end = new Date().getTime();
    console.log(`p4: ${(end - start)/1000}s`);
    resolve(i);
});
Promise.all([p1, p2, p3, p4]).then(value => { 
  console.log(value);
});

На выходе:

p1: 11.456s
p2: 11.426s
p3: 11.44s
p4: 11.438s
[ 10000000000, 10000000000, 10000000000, 10000000000 ]

Получается, параллельно промисы все же не выполняются. Почему же тогда с setTimeout() получается параллельно?

Answer 1

Promise.all - НЕ выполняет промисы параллельно, он их вообще не выполняет. Данная функция возвращает новый Promise, который разрешается когда все переданные Promise успешно завершились.

"Запускается" Promise в момент вызова конструктора, при этом функция, которая передается в конструктор выполняется сразу же синхронно, например:

console.log('before one ctr'); 
var p1 = new Promise((resolve, reject) => { 
  console.log('one ctr'); 
  setTimeout(resolve, 2000, "one"); 
}); 
console.log('before two ctr'); 
var p2 = new Promise((resolve, reject) => { 
  console.log('two ctr'); 
  setTimeout(resolve, 1000, "two"); 
}); 
console.log('before three ctr'); 
var p3 = new Promise((resolve, reject) => { 
  console.log('three ctr'); 
  setTimeout(resolve, 2000, "three"); 
}); 
console.log('before four ctr'); 
var p4 = new Promise((resolve, reject) => { 
  console.log('four ctr'); 
  setTimeout(resolve, 1000, "four"); 
}); 
 
 
Promise.all([p1, p2, p3, p4]).then(value => { 
  console.log(value); 
});

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

Answer 2

JavaScript код всегда синхронный (выполняется в одном потоке). Асинхронно выполняется исключительно ожидание коллбэка из нативного кода браузера (HTTP запросы, чтение файлов, таймеры и т.д. исполняются в отдельных потоках условного браузера). Результат асинхронного вызова встает с коллбэком в очередь - цикл событий.

В итоге, нагрузив синхронный поток исполнения JavaScript вы на корню убили асинхронность, так как результаты и коллбэки просто не могут поступить на исполнение.

const sleep = (timeout = 1000) => new Promise(resolve => setTimeout(resolve, timeout, timeout));
const log = (timeout) => console.log('Finished', timeout);
sleep(3).then(log)
sleep(1).then(log)
sleep(0).then(log)
Finished 1
Finished 0
Finished 3

Promise.all позволяет запустить на ожидание коллбека параллельное сразу несколько промисов. Это его основное отличие от конструкции async/await.

const logAsync = async (timeout = 1000) => {
    console.log('Finished async', await sleep(timeout));
}
(async function() {
    await logAsync(3);
    await logAsync(1);
    await logAsync(0);
})();
Finished async 3
Finished async 1
Finished async 0

Promise.all срабатывает вместе с последним разрешенным промисом - именно это обеспечивает параллельное ожидание коллбеков. Браузер не ждет исполнения промисов поочередно и сразу перескакивает на новый из массива.

(async function() {
    console.time('Promise(all)');
    await Promise.all([sleep(3), sleep(1), sleep(0)]);
    console.timeEnd('Promise(all)');
})();
Promise(all): 3.877197265625ms
READ ALSO
Как сохранить работу (вкл/выкл) расширение в браузере после перезагрузки страницы

Как сохранить работу (вкл/выкл) расширение в браузере после перезагрузки страницы

Чтобы когда плагин включен все работало даже после перезагрузки страницы и отменял изменения после выключения

85
Рекурсия в функции draw

Рекурсия в функции draw

Пожалуйста, помогите дописать код, чтобы двоеточие в часах мигало, а время обновлялось без перезагрузки страницы

95