Асинхронные задачи
Практические асинхронные задачи — promisify, комбинаторы промисов, event emitter, повторные запросы, поллинг и сериализация записей.
9 вопросов
MiddleКодОчень частоРеализовать Promise.all с нуля
Реализовать Promise.all с нуля
Верните new Promise; если вход пуст, сразу resolve []. Ведите счётчик remaining и массив results. Для каждого элемента: Promise.resolve(item).then(value => { results[i] = value; if (--remaining === 0) resolve(results); }, reject). Запись по индексу сохраняет порядок несмотря на тайминг завершения, а первый reject разрешает внешний промис (поздние отклонения игнорируются).
Типичные ошибки
- ✗Делать push результатов вместо записи по индексу, путая порядок при завершении вразнобой
- ✗Забыть случай пустого входа, который без раннего
[]никогда не разрешится - ✗Не оборачивать элементы в
Promise.resolve, из-за чего обычные не-промисы ломают.then
Уточняющие вопросы
- →Как изменить это, чтобы реализовать
Promise.allSettledвместо текущего поведения? - →Зачем оборачивать каждый элемент в
Promise.resolveдля не-промис значений?
MiddleКодЧастоРеализовать цепочечный EventEmitter с on, off и emit
Реализовать цепочечный EventEmitter с on, off и emit
Храните Map от имени события к Set слушателей. on добавляет слушателя и возвращает this; off удаляет его и возвращает this. emit идёт по копии множества слушателей, оборачивая каждый вызов в try/catch, чтобы один бросивший слушатель не прервал остальных. Копирование перед обходом также делает безопасным добавление и удаление слушателей во время emit.
Типичные ошибки
- ✗Позволять одному бросившему слушателю прервать остальных вместо изоляции каждого вызова
- ✗Обходить живую коллекцию слушателей, ломая добавление/удаление во время emit
- ✗Забыть вернуть
thisизon/off, что ломает цепочку вызовов
Уточняющие вопросы
- →Как добавить
once(event, fn), который сам снимается после первого emit? - →Зачем копировать множество слушателей перед обходом внутри
emit?
MiddleКодЧастоРеализовать fetchRetry с ограниченным числом повторов и задержкой
Реализовать fetchRetry с ограниченным числом повторов и задержкой
Оберните попытку в рекурсивный (или циклический) помощник. try { return await fetch(url); } catch (err): если retries <= 0 — пробросить последнюю ошибку; иначе await таймера на delay мс (промис вокруг setTimeout) и рекурсия с retries - 1.
Типичные ошибки
- ✗Цикл без счётчика, повторяющий вечно на постоянно падающем URL
- ✗Забыть
awaitтаймера задержки, из-за чего повторы идут подряд - ✗Проглотить последнюю ошибку вместо отклонения по исчерпании повторов
Уточняющие вопросы
- →Как добавить экспоненциальный backoff вместо фиксированной задержки?
- →Почему стоит повторять лишь на определённых кодах статуса, а не на каждом сбое?
MiddleКодЧастоПостроить поле автоподсказок поиска, отменяющее устаревшие запросы в полёте
Построить поле автоподсказок поиска, отменяющее устаревшие запросы в полёте
Держать AbortController на уровне модуля. На каждый ввод: trim() и выйти, если пусто; прервать предыдущий контроллер, создать новый и fetch с его signal. Поскольку прерывание отменяет старый запрос в полёте, медленный ранний ответ уже не может перезаписать более новый — гонка с порядком ответов устранена. Обернуть fetch в try/catch, игнорировать AbortError, при других ошибках ничего не отрисовывать. В пару — debounce.
Типичные ошибки
- ✗Игнорировать гонку запросов, из-за чего медленный старый ответ перезаписывает подсказки более нового
- ✗Давать
AbortErrorвсплыть как настоящей ошибке и очищать список, когда запрос отменили намеренно - ✗Запрашивать при пустом терме или из пробелов вместо раннего выхода
Уточняющие вопросы
- →Как прерывание предыдущего запроса не даёт ответу не в том порядке победить?
- →Почему
AbortErrorстоит обрабатывать иначе, чем сетевую ошибку, в блоке catch?
JuniorКодИногдаОбернуть функцию с error-first колбэком, чтобы она возвращала промис
Обернуть функцию с error-first колбэком, чтобы она возвращала промис
Верните обёртку, создающую new Promise и вызывающую fn.apply(this, [...args, cb]), где cb = (err, result) => err ? reject(err) : resolve(result). Обычная функция для обёртки (не стрелочная) и apply(this, …) сохраняют исходный this, а добавление колбэка последним соответствует error-first-соглашению.
Типичные ошибки
- ✗Использовать стрелочную функцию для обёртки, теряя
thisвызывающего - ✗Забыть про reject — молча проглатывая аргумент
errиз error-first колбэка - ✗Вызывать
fn(args)вместо добавления колбэка последним аргументом
Уточняющие вопросы
- →Почему обёртка должна быть обычной, а не стрелочной функцией, чтобы сохранить
this? - →Как обработать колбэк, отдающий несколько аргументов результата?
MiddleКодИногдаПостроить клиент аналитики, буферизующий события и отправляющий их пачками
Построить клиент аналитики, буферизующий события и отправляющий их пачками
Держать массив events; queueEvent добавляет { ...event, timestamp: Date.now() }. Самоперепланируемый setTimeout зовёт sendNow, который выходит при пустом буфере, POST-ит события одним JSON-массивом и очищает буфер ТОЛЬКО при успехе — поэтому отклонённый fetch оставляет события в очереди для повтора. sendNow также вызывается напрямую для немедленного сброса.
Типичные ошибки
- ✗Очищать буфер до успешной отправки, из-за чего неудачный POST молча теряет события
- ✗Отправлять поэлементно вместо пачки, что обесценивает буфер
- ✗Использовать
setInterval, позволяя медленному сбросу перекрыть следующий
Уточняющие вопросы
- →Почему буфер надо очищать лишь после успешной отправки, а не до?
- →Как пакетирование событий снижает нагрузку по сравнению с немедленной отправкой каждого?
MiddleКодИногдаРеализовать Promise.any с нуля
Реализовать Promise.any с нуля
Верните new Promise. Ведите счётчик pending и массив errors размером со вход. Для каждого элемента: Promise.resolve(item).then(resolve, err => { errors[i] = err; if (--pending === 0) reject(new AggregateError(errors)); }). Первое выполнение побеждает.
Типичные ошибки
- ✗Путать инверсию: резолвить на отклонении вместо выполнения
- ✗Отклонять на первом отклонении, а не ждать, пока отклонятся все
- ✗Забыть случай пустого входа, который должен отклониться с
AggregateError
Уточняющие вопросы
- →Чем
Promise.anyотличается отPromise.raceпри отклонении? - →Почему пустой вход отклоняется, а не остаётся pending навсегда?
MiddleКодИногдаПостроить клиент фича-флагов, опрашивающий обновления, с управлением start/stop
Построить клиент фича-флагов, опрашивающий обновления, с управлением start/stop
Планировать через setTimeout, а не setInterval: в колбэке сделать await запроса, затем перепланировать в .finally, чтобы медленный запрос задерживал следующий опрос, а не перекрывал его. forceUpdate обновляет сейчас; getToggle читает data[key]; stop зовёт clearTimeout; start снова взводит таймер. Самоперепланирование лишь после завершения и не даёт перекрыться обновлениям в полёте.
Типичные ошибки
- ✗Использовать
setInterval, который может наслаивать перекрывающиеся запросы, когда запрос медленнее интервала - ✗Перепланировать до разрешения запроса, а не в
.finally, снова порождая перекрытие - ✗Забывать
clearTimeoutвstop(), оставляя опрос всё ещё ожидающим
Уточняющие вопросы
- →Почему самоперепланируемый
setTimeoutизбегает перекрытия запросов, которое допускаетsetInterval? - →Почему перепланировать в
.finally, а не в обработчике успеха?
SeniorКодИногдаСериализовать async-записи так, чтобы две не шли одновременно
Сериализовать async-записи так, чтобы две не шли одновременно
Держите промис tail для конца очереди, начиная с Promise.resolve(). В enqueue(data) цепляйте запись: const run = tail.then(() => write(data)). Продвигайте tail = run.catch(() => {}), чтобы одно отклонение не отравило последующие записи, и возвращайте run вызывающему.
Типичные ошибки
- ✗Продвигать tail без
catch, из-за чего одно отклонение ломает всю очередь - ✗Возвращать общий tail, так что каждый вызывающий видит результат чужой записи
- ✗Запускать записи одновременно вместо цепляния каждой на предыдущую
Уточняющие вопросы
- →Почему tail должен проглатывать ошибки, а возвращаемый промис — сохранять их?
- →Как добавить лимит длины очереди, отклоняющий новые постановки?