У меня есть массив обещаний, которые я хотел бы вызывать параллельно, но разрешать синхронно.
Я сделал этот фрагмент кода для выполнения требуемой задачи, однако мне нужно было создать свой собственный объект QueryablePromise, чтобы обернуть собственный Promise, который я могу синхронно проверить, чтобы увидеть его разрешенный статус.
Есть ли лучший способ выполнить эту задачу, не требующий специального объекта?
Please note. I do not want to use
Promise.allas I don't want to have to wait for all promises to resolve before processing the effects of the promises. And I cannot useasyncfunctions 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>Спасибо за любую помощь.
@cartant спасибо за ваш комментарий, у вас есть пример того, как это будет работать?
Я считаю, что требуется специальный объект, так как мне нужно синхронно проверять статус обещаний с моим кодом выше. Вы не можете сделать это с собственным Promise, поэтому мне нужно было обернуть его с помощью QueryablePromise, что дает мне возможность проверить promise.resolved в обратном вызове. . Я имею в виду, что лучше не обертывать обещание, мне интересно, есть ли функциональное соглашение о коде js или конкретный объект, который выполнит задачу, или есть ли проверенная библиотека, которую я могу использовать для выполнения той же задачи.
@ synthet1c Вы можете использовать .then(), см. этот отвечать в Подождите, пока все обещания ES6 будут выполнены, даже отклоненные обещания; этот шаблон с использованием jQuery также можно использовать без jQuery Jquery Ajax предотвращает сбой в отложенном последовательном цикле
@guest271314 guest271314 Я использовал аналогичную концепцию с QueryablePromise, мой просто немного более подробный, кажется хорошей идеей уменьшить требуемый код, единственная проблема в том, что вы не можете связать обратные вызовы с нативным обещанием, так как вам нужно установить status перед вызовом каких-либо дальнейших обратных вызовов, это означает, что вы не можете абстрагироваться от этой части основного кода. И вы не можете проверить статус снаружи внутренних обещаний.
Предлагаю указать, чего вы пытаетесь достичь, и сократить код до самого необходимого. Если бы async/await был вариантом, он указывал бы на этот шаблон, который может быть даже более подробным, чем код в отвечать этого пользователя для этого вопроса Запускать несколько рекурсивных промисов и прерывать их по запросу, хотя может выполнить то, что вы пытаетесь сделать.
@synthet1c См. также Как обрабатывать несколько сценариев браузера, выполняющих одни и те же вызовы серверной службы
Смотрите ответы на Обещания для обещаний, которые еще предстоит создать без использования отложенного [анти]паттерна. Вы можете передать callback в «очередь» по этому отвечать, который является обновленной версией кода в ответе этого пользователя по предыдущей ссылке.
@guest271314 guest271314 На самом деле, я думаю, вы были правы, я слишком усложнял код с помощью QueryablePromise. Я просто использовал внутренний массив для сохранения результатов, а затем проверял этот массив на предмет статуса обещаний, а не имел состояние самого обещания.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Используя цикл 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, чтобы я мог продолжить цепочку в конце концов обещания решены. Спасибо за помощь.
Я бы рекомендовал действительно использовать 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(), как здесь?
Я пошел вперед и внес изменения, о которых я просил. Если вы считаете, что мое редактирование недостаточно отличается от вашего ответа, пожалуйста, не стесняйтесь отменить его.
@PatrickRoberts Я рад, что вам это нравится :-) На самом деле я чувствую, что ваше обновление все еще недостаточно похоже: chain.then(() => promise) вызывает предупреждение о необработанном отклонении, если promise отклоняется до выполнения chain - действительно следует использовать Promise.all.
Посмотрите во второй части моего ответа правильную обработку ошибок. Независимо от порядка, в котором устанавливаются chain и promise, отклонение по-прежнему обрабатывается вторым аргументом следующего вызова then().
А, теперь я понимаю, о чем вы говорите. Предупреждение об отказе — это всего лишь предупреждение. Поскольку код в конечном итоге обрабатывает отклонение, в этом нет ничего плохого. Просто браузер недостаточно сложен, чтобы на самом деле выдавать предупреждение, когда обещание является сборщиком мусора без обработки его отклонения, а скорее выдает преждевременно, а затем обрабатывается после.
Кроме того, еще один момент заключается в том, что поведение chain.then(() => promise)намеренно отличается от поведения Promise.all([chain, promise]). Он гарантированно будет урегулирован только после того, как chain установится, даже если promise отклонит его раньше, тогда как Promise.all() урегулирует, как только promise отклонит его, даже если chain не урегулировал.
@PatrickRoberts Хорошо, я не был уверен, каково было намерение, но если вы хотите обработать исключение позже (в порядке массива), вам следует подавить потенциальное предупреждение с помощью явного promise.catch(e => {/* ignore til later*/});.
Спасибо, я тоже так делал.
Рассмотрите возможность создания массива обещаний, в котором каждое обещание включает свои эффекты, и передайте этот массив в
Promise.all.