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

У меня есть массив обещаний, которые я хотел бы вызывать параллельно, но разрешать синхронно.

Я сделал этот фрагмент кода для выполнения требуемой задачи, однако мне нужно было создать свой собственный объект QueryablePromise, чтобы обернуть собственный Promise, который я могу синхронно проверить, чтобы увидеть его разрешенный статус.

Есть ли лучший способ выполнить эту задачу, не требующий специального объекта?

Please note. I do not want to use Promise.all as I don't want to have to wait for all promises to resolve before processing the effects of the promises. And I cannot use async functions in my code base.

const PROMISE = Symbol('PROMISE')
const tap = fn => x => (fn(x), x)

class QueryablePromise {
  resolved = false
  rejected = false
  fulfilled = false
  constructor(fn) {
    this[PROMISE] = new Promise(fn)
      .then(tap(() => {
        this.fulfilled = true
        this.resolved = true
      }))
      .catch(x => {
        this.fulfilled = true
        this.rejected = true
        throw x
      })
  }
  then(fn) {
    this[PROMISE].then(fn)
    return this
  }
  catch(fn) {
    this[PROMISE].catch(fn)
    return this
  }
  static resolve(x) {
    return new QueryablePromise((res) => res(x))
  }
  static reject(x) {
    return new QueryablePromise((_, rej) => rej(x))
  }
}

/**
 * parallelPromiseSynchronousResolve
 *
 * Call array of promises in parallel but resolve them in order
 *
 * @param  {Array<QueryablePromise>}  promises
 * @praram {Array<fn>|fn}  array of resolver function or single resolve function
 */
function parallelPromiseSynchronousResolve(promises, resolver) {
  let lastResolvedIndex = 0
  const resolvePromises = (promise, i) => {
    promise.then(tap(x => {
      // loop through all the promises starting at the lastResolvedIndex
      for (; lastResolvedIndex < promises.length; lastResolvedIndex++) {
        // if promise at the current index isn't resolved break the loop
        if (!promises[lastResolvedIndex].resolved) {
          break
        }
        // resolve the promise with the correct resolve function
        promises[lastResolvedIndex].then(
          Array.isArray(resolver)
            ? resolver[lastResolvedIndex]
            : resolver
        )
      }
    }))
  }
  
  promises.forEach(resolvePromises)
}

const timedPromise = (delay, label) => 
  new QueryablePromise(res => 
    setTimeout(() => {
      console.info(label)
      res(label)
    }, delay)
  )

parallelPromiseSynchronousResolve([
  timedPromise(20, 'called first promise'),
  timedPromise(60, 'called second promise'),
  timedPromise(40, 'called third promise'),
], [
  x => console.info('resolved first promise'),
  x => console.info('resolved second promise'),
  x => console.info('resolved third promise'),
])
<script src = "https://codepen.io/synthet1c/pen/KyQQmL.js"></script>

Спасибо за любую помощь.

Рассмотрите возможность создания массива обещаний, в котором каждое обещание включает свои эффекты, и передайте этот массив в Promise.all.

cartant 10.02.2019 21:12

@cartant спасибо за ваш комментарий, у вас есть пример того, как это будет работать?

synthet1c 10.02.2019 21:16
«Есть ли лучший способ выполнить эту задачу, не требующий специального объекта?» Что вы подразумеваете под "лучше"? Как вы думаете, почему требуется "специальный объект"?
guest271314 10.02.2019 21:20

Я считаю, что требуется специальный объект, так как мне нужно синхронно проверять статус обещаний с моим кодом выше. Вы не можете сделать это с собственным Promise, поэтому мне нужно было обернуть его с помощью QueryablePromise, что дает мне возможность проверить promise.resolved в обратном вызове. . Я имею в виду, что лучше не обертывать обещание, мне интересно, есть ли функциональное соглашение о коде js или конкретный объект, который выполнит задачу, или есть ли проверенная библиотека, которую я могу использовать для выполнения той же задачи.

synthet1c 10.02.2019 21:25

@ synthet1c Вы можете использовать .then(), см. этот отвечать в Подождите, пока все обещания ES6 будут выполнены, даже отклоненные обещания; этот шаблон с использованием jQuery также можно использовать без jQuery Jquery Ajax предотвращает сбой в отложенном последовательном цикле

guest271314 10.02.2019 21:29

@guest271314 guest271314 Я использовал аналогичную концепцию с QueryablePromise, мой просто немного более подробный, кажется хорошей идеей уменьшить требуемый код, единственная проблема в том, что вы не можете связать обратные вызовы с нативным обещанием, так как вам нужно установить status перед вызовом каких-либо дальнейших обратных вызовов, это означает, что вы не можете абстрагироваться от этой части основного кода. И вы не можете проверить статус снаружи внутренних обещаний.

synthet1c 10.02.2019 21:36

Предлагаю указать, чего вы пытаетесь достичь, и сократить код до самого необходимого. Если бы async/await был вариантом, он указывал бы на этот шаблон, который может быть даже более подробным, чем код в отвечать этого пользователя для этого вопроса Запускать несколько рекурсивных промисов и прерывать их по запросу, хотя может выполнить то, что вы пытаетесь сделать.

guest271314 10.02.2019 21:44

Смотрите ответы на Обещания для обещаний, которые еще предстоит создать без использования отложенного [анти]паттерна. Вы можете передать callback в «очередь» по этому отвечать, который является обновленной версией кода в ответе этого пользователя по предыдущей ссылке.

guest271314 10.02.2019 22:13

@guest271314 guest271314 На самом деле, я думаю, вы были правы, я слишком усложнял код с помощью QueryablePromise. Я просто использовал внутренний массив для сохранения результатов, а затем проверял этот массив на предмет статуса обещаний, а не имел состояние самого обещания.

synthet1c 10.02.2019 22:27
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
10
763
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Используя цикл for await...of, вы можете сделать это довольно хорошо, если у вас уже есть массив промисов:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

(async () => {
  const promises = range(5, index => {
    const ms = Math.round(Math.random() * 5000);
    return delay(ms).then(() => ({ ms, index }));
  });

  const start = Date.now();

  for await (const { ms, index } of promises) {
    console.info(`index ${index} resolved at ${ms}, consumed at ${Date.now() - start}`);
  }
})();

Поскольку вы не можете использовать асинхронные функции, вы можете имитировать эффект for await...of, объединяя промисы вместе с помощью Array.prototype.reduce() и синхронно планируя обратный вызов для каждой цепочки:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

const asyncForEach = (array, cb) => array.reduce(
  (chain, promise, index) => chain.then(
    () => promise
  ).then(
    value => cb(value, index)
  ),
  Promise.resolve()
);

const promises = range(5, index => {
  const ms = Math.round(Math.random() * 5000);
  return delay(ms).then(() => ms);
});

const start = Date.now();

asyncForEach(promises, (ms, index) => {
  console.info(`index ${index} resolved at ${ms}, consumed at ${Date.now() - start}`);
});

Обработка ошибок

Поскольку промисы были созданы параллельно, я предполагаю, что ошибки в каждом отдельном промисе не будут распространяться на другие промисы, кроме как через какие-либо потенциально хрупкие цепочки, построенные через asyncForEach() (как выше).

Но мы также хотим избежать перекрестного распространения ошибок между промисами при объединении их в цепочку на asyncForEach(). Вот способ надежно запланировать обратные вызовы ошибок, когда ошибки могут распространяться только из исходных промисов:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const maybe = p => p.then(v => Math.random() < 0.5 ? Promise.reject(v) : v);
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

const asyncForEach = (array, fulfilled, rejected = () => {}) => array.reduce(
  (chain, promise, index) => {
    promise.catch(() => {}); // catch early rejection until handled below by chain
    return chain.then(
      () => promise,
      () => promise // catch rejected chain and settle with promise at index
    ).then(
      value => fulfilled(value, index),
      error => rejected(error, index)
    );
  },
  Promise.resolve()
);

const promises = range(5, index => {
  const ms = Math.round(Math.random() * 5000);
  return maybe(delay(ms).then(() => ms)); // promises can fulfill or reject
});

const start = Date.now();

const settled = state => (ms, index) => {
  console.info(`index ${index} ${state}ed at ${ms}, consumed at ${Date.now() - start}`);
};

asyncForEach(
  promises,
  settled('fulfill'),
  settled('reject') // indexed callback for rejected state
);

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

Привет, Патрик, да, я тоже это заметил и добавил это в вашу функцию asyncForEach, и я добавил возможность обрабатывать каждый ответ обещания с помощью отдельного обратного вызова или с одним обратным вызовом и обернул всю функцию в Promise.all, чтобы я мог продолжить цепочку в конце концов обещания решены. Спасибо за помощь.

synthet1c 11.02.2019 22:24

Я бы рекомендовал действительно использовать Promise.all - но не по всем обещаниям сразу, а по всем обещаниям, которые вы хотите выполнить для каждого шага. Вы можете создать этот "древовидный список" промисов с помощью reduce:

function parallelPromisesSequentialReduce(promises, reducer, initial) {
  return promises.reduce((acc, promise, i) => {
    return Promise.all([acc, promise]).then(([prev, res]) => reducer(prev, res, i));
  }, Promise.resolve(initial));
}

const timedPromise = (delay, label) => new Promise(resolve =>
  setTimeout(() => {
    console.info('fulfilled ' + label + ' promise');
    resolve(label);
  }, delay)
);

parallelPromisesSequentialReduce([
  timedPromise(20, 'first'),
  timedPromise(60, 'second'),
  timedPromise(40, 'third'),
], (acc, res) => {
  console.info('combining ' + res + ' promise with previous result (' + acc + ')');
  acc.push(res);
  return acc;
}, []).then(res => {
  console.info('final result', res);
}, console.error);

Интересный подход! Не возражаете, если я отредактирую свои реализации asyncForEach(), чтобы использовать reduce() без map(), как здесь?

Patrick Roberts 12.02.2019 00:32

Я пошел вперед и внес изменения, о которых я просил. Если вы считаете, что мое редактирование недостаточно отличается от вашего ответа, пожалуйста, не стесняйтесь отменить его.

Patrick Roberts 12.02.2019 00:52

@PatrickRoberts Я рад, что вам это нравится :-) На самом деле я чувствую, что ваше обновление все еще недостаточно похоже: chain.then(() => promise) вызывает предупреждение о необработанном отклонении, если promise отклоняется до выполнения chain - действительно следует использовать Promise.all.

Bergi 12.02.2019 09:50

Посмотрите во второй части моего ответа правильную обработку ошибок. Независимо от порядка, в котором устанавливаются chain и promise, отклонение по-прежнему обрабатывается вторым аргументом следующего вызова then().

Patrick Roberts 12.02.2019 09:55

А, теперь я понимаю, о чем вы говорите. Предупреждение об отказе — это всего лишь предупреждение. Поскольку код в конечном итоге обрабатывает отклонение, в этом нет ничего плохого. Просто браузер недостаточно сложен, чтобы на самом деле выдавать предупреждение, когда обещание является сборщиком мусора без обработки его отклонения, а скорее выдает преждевременно, а затем обрабатывается после.

Patrick Roberts 12.02.2019 10:07

Кроме того, еще один момент заключается в том, что поведение chain.then(() => promise)намеренно отличается от поведения Promise.all([chain, promise]). Он гарантированно будет урегулирован только после того, как chain установится, даже если promise отклонит его раньше, тогда как Promise.all() урегулирует, как только promise отклонит его, даже если chain не урегулировал.

Patrick Roberts 12.02.2019 10:16

@PatrickRoberts Хорошо, я не был уверен, каково было намерение, но если вы хотите обработать исключение позже (в порядке массива), вам следует подавить потенциальное предупреждение с помощью явного promise.catch(e => {/* ignore til later*/});.

Bergi 12.02.2019 12:57

Спасибо, я тоже так делал.

Patrick Roberts 12.02.2019 17:13

Другие вопросы по теме