Рассмотрим этот простой пример, вероятно, функцию, которую вы писали пару раз, но теперь ее можно прервать:
/**
*
* @param {number} delay
* @param {AbortSignal} [abortSignal]
* @returns {Promise<void>}
*/
export default function timeoutPromise(delay, abortSignal) {
return new Promise((resolve, reject) => {
if (abortSignal) {
abortSignal.throwIfAborted();
}
const timeout = setTimeout(() => {
resolve();
}, delay);
abortSignal.addEventListener("abort", () => {
clearTimeout(timeout);
reject(new Error("Aborted"));
});
});
}
Очевидная проблема заключается в том, что это не очистит eventListener, если тайм-аут пройдет нормально. Это можно сделать, но это довольно некрасиво:
/**
*
* @param {number} delay
* @param {AbortSignal} [abortSignal]
* @returns {Promise<void>}
*/
export default function timeoutPromise(delay, abortSignal) {
return new Promise((resolve, reject) => {
// note: changed to reject() to get consistent behavior regardless of the signal state
if (abortSignal && abortSignal.aborted) {
reject(new Error("timeoutPromise aborted"));
}
let timeout = null;
function abortHandler() {
clearTimeout(timeout);
reject(new Error("timeoutPromise aborted"))
}
timeout = setTimeout(() => {
if (abortSignal) {
abortSignal.removeEventListener("abort", abortHandler);
}
resolve();
}, delay);
if (abortSignal) {
abortSignal.addEventListener("abort", abortHandler, {once: true});
}
});
}
Это... много кода для такой простой вещи. Правильно ли я делаю или есть лучший способ?
Почему вы больше не используете abortSignal.throwIfAborted();? И что вообще с if (abortSignal)?
Почему вы хотите удалить прослушиватель событий? Если никто не имеет ссылки на AbortSignal, он будет удален вместе с ним.
Также можно отметить, что именно для этого случая прерываемого тайм-аута есть один (в настоящее время только в Chrome): Scheduler.postTask(cb, {delay, signal}) и он даже возвращает обещание.
@Kaiido, потому что сигнал об отмене часто намного превышает тайм-аут
@Берги и? Прикрепление к нему события добавляет очень мало накладных расходов по сравнению со всем AbortController.
@Kaiido Это все еще утечка памяти. Обработчик событий abort закрывается по timeout, reject и — в зависимости от реализации обещания — даже по обещанию и его значению. Вы не хотите, чтобы долго выполняющаяся задача с множеством небольших задержек накапливала множество обработчиков для сигнала прерывания — обычно только последний из них действительно может прервать задачу.
@Kaiido Это всего лишь простой пример. В общем, сохранение прослушивателя может захватывать произвольное количество переменных с помощью обратного вызова. Кроме того, утечка памяти только потому, что вы думаете, что утечка мала, на самом деле не является хорошим подходом и не тем, чего вы ожидаете от «элегантного» решения.
@Bergi Я удалил throwIfAborted, потому что это приводит к непоследовательному поведению. Если сигнал прерван, он выдается при создании обещания, если сигнал был отменен мгновением позже, он выдает только await. Это актуально, когда вы ожидаете обещаний позже, а не момента их создания, что необходимо для одновременного выполнения задач. if (abortSignal) присутствует, потому что сигнал должен быть необязательным.
@TomášZato "выдает при создании обещания" - нет, signal.throwIfAborted() в обратном вызове исполнителя точно эквивалентно if (signal.aborted) reject(signal.reason). "когда вы ожидаете обещаний позже, а не момента их создания, что необходимо для одновременного выполнения задач" - нет, это не имело бы значения. И в любом случае этого делать никогда не стоит! Всегда используйте Promise.all для параллельных задач.
@Bergi Если вы ждете Promise.all, то общее время вашей работы — это время на выполнение самого медленного обещания плюс сумма времени на обработку результатов. Если вы используете Promise.race для обработки ответов по мере их поступления, вы можете обрабатывать ответы параллельно с ожиданием промисов, но в этом случае вам нужно где-то хранить невыполненные промисы. Совсем недавно я слышал, как коллега рассказывал об ошибке, когда кто-то слепо следовал правилу Promise.all, что вызывало серьезные узкие места в системе.
@TomášZato Вы по-прежнему можете обрабатывать результаты по мере их поступления с помощью Promise.all, просто поместите код обработки прямо там, где вы делаете каждый запрос (Promise.all(vals.map(async v => { const response = await request(v); return handle(response); }))). Нет необходимости ставить Promise.race или держать невыполненные обещания в течение некоторого времени.
красиво и познавательно, и... вопрос и ответ.
@Bergi Хорошо, это хороший момент, ты можешь это сделать. Думаю, я больше думал о конкретном примере, над которым работаю, где количество обещаний очень велико, поэтому я начинаю некоторые, использую Promise.race и заменяю выполненные новыми. Вероятно, это не то, о чем стоит беспокоиться типичному веб-разработчику. Я говорю о миллионах задач, выполняющих максимум N параллельно.
@TomášZato Конечно, есть устоявшиеся шаблоны для решения этой проблемы, ни одно из них не требует невыполненных обещаний, оставшихся висеть



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


Вы можете использовать необязательную цепочку для всех вызовов метода AbortSignal, и это станет более простым:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
function done() {
resolve();
signal?.removeEventListener("abort", stop);
}
function stop() {
reject(this.reason);
clearTimeout(handle);
}
signal?.throwIfAborted();
const handle = setTimeout(done, ms);
signal?.addEventListener("abort", stop);
});
}
(from my answer to How to cancel JavaScript sleep?)
Либо сделать только одну проверку на наличие сигнала:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
if (!signal) {
setTimeout(resolve, ms);
return;
}
function done() {
resolve();
signal.removeEventListener("abort", stop);
}
function stop() {
reject(this.reason);
clearTimeout(handle);
}
signal.throwIfAborted();
const handle = setTimeout(done, ms);
signal.addEventListener("abort", stop);
});
}
что вы, конечно, можете играть в гольф дальше:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
if (!signal) return setTimeout(resolve, ms);
signal.throwIfAborted();
const handle = setTimeout(() => {
resolve();
signal.removeEventListener("abort", stop);
}, ms);
const stop = () => {
reject(signal.reason);
clearTimeout(handle);
};
signal.addEventListener("abort", stop);
});
}
a lot of codeнаписано немного по-другому, фактически это две дополнительные строки кода