Внутри Promise
Promise — это конечный автомат, урегулируемый единожды, который хранит состояние, значение и список записей PromiseReaction. .then регистрирует реакции и возвращает новый Promise; урегулирование ставит по PromiseReactionJob на реакцию. Урегулирование thenable стоит лишнего тика.
Вы пишете await fetchUser() и await fetchPosts(), и кто-то спрашивает: сколько тиков микрозадач это стоило? Большинство инженеров пожмут плечами. Но ответ решает, добавляет ли горячий асинхронный путь один тик или четыре на каждую итерацию — а при 10 000 итераций на рендер этот разрыв и есть разница между плавным списком и дёрганым. Чтобы считать тики, нужно знать, что такое Promise внутри.
Promise — это три поля и список
Снимите API, и объект Promise в V8 окажется маленьким. Его существенные внутренние слоты:
[[PromiseState]]— одно изpending,fulfilled,rejected. Начинается сpendingи переходит ровно один раз. Этот инвариант урегулирования единожды — сердце автомата: после ухода изpendingникакой более позднийresolve/rejectего не изменит.[[PromiseResult]]— значение, которым он fulfilled, или причина, которой rejected. Бессмысленно, покаpending.[[PromiseFulfillReactions]]/[[PromiseRejectReactions]]— два списка записей PromiseReaction, обработчиков, зарегистрированных через.then/.catch, пока Promise был ещё pending. После урегулирования эти списки очищаются и заменяются результатом.
Запись PromiseReaction связывает три вещи: тип (fulfill или reject), обработчик (ваш колбэк onFulfilled / onRejected или undefined) и capability нового Promise, который вернул .then (функции resolve/reject, которые урегулируют этот нижестоящий Promise). Эта последняя часть — причина, по которой сцепление вообще работает.
Что на самом деле делает .then
promise.then(onF, onR) делает три конкретные вещи по порядку:
- Создаёт новый Promise («result capability») и запоминает его функции resolve/reject.
- Строит две записи PromiseReaction — одну fulfill, одну reject — каждая оборачивает совпадающий обработчик и эту новую capability.
- Либо регистрирует, либо планирует их. Если исходный Promise ещё
pending, реакции добавляются в его списки реакций и пока ничего не запускается. Если Promise уже урегулирован,.thenне запускает обработчик синхронно — он немедленно ставитPromiseReactionJobкак микрозадачу для совпадающей реакции.
const p = Promise.resolve(42); // уже fulfilled значением 42
const q = p.then(v => v + 1); // q — НОВЫЙ Promise; одна PromiseReactionJob поставлена сейчас
// q урегулируется значением 43 после того, как этот job выполнится (одной микрозадачей позже)Итак, .then никогда не запускает ваш обработчик «прямо сейчас», даже на уже урегулированном Promise. Он всегда откладывает на микрозадачу. Эта единственная гарантия и делает порядок Promise предсказуемым: колбэк .then всегда отстоит на одну микрозадачу от урегулирования, никогда не синхронен.
Урегулирование ставит по одному job на реакцию
Когда продюсер вызывает функцию resolve (урегулируя Promise в fulfilled), движок выполняет FulfillPromise: устанавливает [[PromiseState]] в fulfilled, сохраняет значение, а затем обходит [[PromiseFulfillReactions]], ставя микрозадачу PromiseReactionJob для каждой зарегистрированной реакции. Каждый job, когда его выполнит чекпоинт микрозадач, вызывает обработчик реакции с результатом и использует возвращённое значение, чтобы урегулировать нижестоящий Promise этой реакции (у которого могут быть собственные реакции, каскадом порождающие новые jobs). Отклонение симметрично через RejectPromise и [[PromiseRejectReactions]].
Вот почему fan-out стоит N микрозадач: p.then(a); p.then(b); p.then(c) на урегулированном p регистрирует три реакции и ставит три PromiseReactionJob — a, b, c выполняются как три отдельные микрозадачи в порядке регистрации.
- Promise.resolve().then(fn) → fn запускается
- 1 тик
- N .then на одном урегулированном Promise
- N тиков
- Цепочка .then().then().then()
- 1 тик на звено
- resolve(promise) — принять thenable
- +1 тик (resolve job)
- resolve(не-нативный thenable)
- +1 тик (thenable job)
- await нативного Promise (V8 7.2+)
- 1 тик (было 3)
Лишний тик: урегулирование через thenable
Вот тонкость, спотыкающая подсчёт тиков. Если вы урегулируете Promise другим Promise (или любым thenable), движок не может принять его значение синхронно — thenable может урегулироваться позже. Поэтому ResolvePromise планирует PromiseResolveThenableJob: микрозадачу, единственная цель которой — вызвать .then внутреннего thenable, чтобы подписаться на него. Эта подписка сама по себе ещё одно откладывание. Следствие: урегулирование Promise A через Promise B добавляет лишний тик микрозадачи по сравнению с урегулированием A обычным значением, потому что движок должен отскочить через PromiseResolveThenableJob, прежде чем A сможет хотя бы начать принимать будущее состояние B.
// Обычное значение: q урегулируется одним тиком после запуска реакции p.
Promise.resolve(1).then(v => v);
// Thenable: возврат Promise из обработчика стоит лишнего тика,
// потому что внешний Promise должен принять внутренний через resolve job.
Promise.resolve(1).then(v => Promise.resolve(v)); // лишний PromiseResolveThenableJobЭто и есть механическая причина, по которой исходный await стоил три тика (разбирается в следующем уроке): спецификация оборачивала ожидаемое значение в одноразовый Promise и принимала его через thenable job. V8 7.2 (2018) сделал особый случай для нативных Promise, пропускающий эти лишние jobs.
Отслеживание необработанных отклонений
Движок также следит за отклонениями без reject-обработчика. Когда выполняется RejectPromise, а список reject-реакций пуст (не прикреплён .catch/.then(_, onR)), Promise помечается как имеющий потенциально необработанное отклонение. Хост уведомляется через HostPromiseRejectionTracker, что и порождает браузерное событие unhandledrejection и process.on('unhandledRejection') в Node. Важно, что это отложено: обработчик, прикреплённый позже в том же обороте, снимает флаг (порождает rejectionhandled), потому что прикрепление .catch регистрирует reject-реакцию, которая потребляет сохранённое отклонение. Это откладывание — причина, по которой «добавьте .catch на следующей строке» всё ещё подавляет предупреждение — а вот .catch, прикреплённый макрозадачей позже, уже нет.
Что создаёт `const q = p.then(fn)`, и когда запустится `fn`, если `p` уже fulfilled?
Почему урегулирование Promise `A` другим Promise `B` стоит лишнего тика микрозадачи против урегулирования `A` числом 5?
Расставьте по порядку, что делает движок от вызова `.then` на ещё pending-Promise до запуска обработчика после урегулирования Promise.
- 1 .then создаёт новый нижестоящий Promise и запоминает его resolve/reject
- 2 .then строит записи PromiseReaction и добавляет их в списки pending-Promise
- 3 Продюсер вызывает resolve(); FulfillPromise сохраняет значение и ставит состояние fulfilled
- 4 FulfillPromise ставит микрозадачу PromiseReactionJob для каждой зарегистрированной реакции
- 5 Чекпоинт микрозадач выполняет каждый job: обработчик запускается и урегулирует нижестоящий Promise
▸Почему это работает
Зачем заставлять .then всегда откладывать на микрозадачу, даже когда Promise уже урегулирован? Потому что функция, которая иногда вызывает свой колбэк синхронно, а иногда асинхронно, неподдаётся рассуждению — печально известное «выпускание Zalgo». Гарантируя, что обработчик всегда запускается в более поздней микрозадаче, Promise дают вам один инвариант, на который можно опереться: код после .then(...) на текущей строке всегда выполняется до обработчика.
- 01Опишите внутреннюю структуру Promise и инвариант урегулирования единожды.
- 02Пройдите всё, что делает .then, и объясните, почему обработчик никогда не синхронен.
- 03Объясните лишний тик от урегулирования Promise другим Promise и свяжите его с await.
Promise — компактный конечный автомат, урегулируемый единожды. Внутри это слот состояния ([[PromiseState]]: pending → fulfilled | rejected, в одну сторону), слот результата ([[PromiseResult]]) и два списка записей PromiseReaction ([[PromiseFulfillReactions]] / [[PromiseRejectReactions]]). Каждая реакция связывает тип, ваш обработчик и capability resolve/reject нового Promise, который вернул .then — поэтому сцепление и композируется. .then всегда возвращает новый Promise и никогда не запускает ваш обработчик синхронно: на pending-Promise он добавляет реакции; на урегулированном немедленно ставит микрозадачу PromiseReactionJob. Урегулирование (FulfillPromise/RejectPromise) обходит совпадающий список и ставит по job на реакцию, поэтому fan-out стоит N микрозадач, а цепочка — один тик на звено. Печально известный лишний тик идёт от урегулирования Promise через thenable: движок планирует PromiseResolveThenableJob, чтобы подписаться на внутренний Promise, прежде чем принять его состояние. Отслеживание необработанных отклонений срабатывает, когда у отклонённого Promise нет reject-реакции, но .catch, добавленный позже в том же обороте, снимает флаг. Знание этой машинерии позволяет считать тики точно. Теперь, когда встретишь асинхронный путь, который тормозит без очевидной причины, — считай вслух: сколько звеньев .then, сколько принятий thenable, сколько реакций в fan-out — и будешь знать, куда уходит бюджет микрозадач ещё до профилировщика.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в184
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- Нормальные формы, денормализация и почему схемы «прилипают»middle
- JSONB, массивы и когда side table побеждаетmiddle
- Heap-хранилище, TOAST и выравнивание колонокsenior
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Реляционная модель vs документные, wide-column, граф и key-valuesenior
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Производственные режимы отказа и стабильность плановsenior
- MVCC: как Postgres раздаёт согласованные снимкиjunior
- Заголовок tuple и механика снимковmiddle
- HOT-обновления и уровни изоляцииmiddle
- VACUUM, bloat и autovacuummiddle
- CLOG, XID wraparound и MultiXactsenior
- SSI и production-тюнинг autovacuumsenior
- Реальные провалы MVCC, deployment-паттерны и распределённые снимкиsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- Что такое миграция схемы и почему она заменяет ad-hoc DDLjunior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Expand-contract: нулевой простой для ломающих изменений схемыmiddle
- Advisory-блокировки, инструменты миграций и координация деплояsenior
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Зачем нужно шардирование: потолок одного Postgresjunior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Партиционирование против шардирования: одно слово, два разных понятияmiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Schema-based шардирование и альтернативы мультиарендностиsenior
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.