Series: JavaScript Basics Lesson 48

Promise ichki mexanizmi

Promise'ning uchta holati ([[PromiseState]]), internal slotlari, executor'ning sinxron ishlashi, resolve/reject closure orqali qanday bog'langani, .then() yangi Promise qaytarishi, Resolution Procedure orqali flatten bo'lishi, .catch() propagation, va .then() nega har doim microtask ekani.

48-dars

Promise ichki mexanizmi

Promise — bu kelajakda tayyor bo'ladigan qiymatning "o'rinbosari" (placeholder). Callback'dan farqi: Promise natijani "funksiya orqali yetkazish" o'rniga, natijani obyekt ichida saqlaydi, va bu obyektning holati o'zgarganda, unga "obuna bo'lgan" funksiyalar avtomatik chaqiriladi.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('natija tayyor'), 1000);
});

promise.then((result) => console.log(result)); // 1 sekunddan keyin: "natija tayyor"

1. Uchta holat (state) — Promise'ning yuragi

Har bir Promise obyekti ichida [[PromiseState]] degan yashirin (internal) maydon bor, va u faqat uchta qiymatdan birini oladi:

pending   → hali natija yo'q, kutilyapti (boshlang'ich holat)
fulfilled → muvaffaqiyatli tugadi, natija bor
rejected  → xato bilan tugadi, sabab bor

Muhim qoida — bir marta o'zgaradi, keyin qotib qoladi:

pending → fulfilled   (qaytib bo'lmaydi)
pending → rejected    (qaytib bo'lmaydi)
new Promise((resolve, reject) => {
  resolve('birinchi');
  resolve('ikkinchi'); // hech narsa qilmaydi — allaqachon fulfilled
  reject('xato');      // bu ham e'tiborsiz qoladi
});

Bu — callback'dagi "ikki marta chaqirilishi mumkin" muammosini til darajasida hal qiladi.


2. Promise obyektining ichki tuzilishi (V8/spec darajasida)

new Promise((resolve, reject) => {...}) chaqirilganda, V8 xotirada quyidagi internal slotlarga ega obyekt yaratadi (bular JS kodidan ko'rinmaydi, faqat DevTools orqali qisman ko'rinadi):

Promise obyekti (heap'da):
┌────────────────────────────────────────┐
│ [[PromiseState]]: "pending"             │  ← holat
│ [[PromiseResult]]: undefined            │  ← natija/sabab (hozircha bo'sh)
│ [[PromiseFulfillReactions]]: []         │  ← .then() bilan qo'shilgan "muvaffaqiyat" callback'lar ro'yxati
│ [[PromiseRejectReactions]]: []          │  ← .then()/.catch() bilan qo'shilgan "xato" callback'lar ro'yxati
│ [[PromiseIsHandled]]: false             │  ← unhandled rejection uchun belgi
└────────────────────────────────────────┘

Executor funksiya — (resolve, reject) => {...} — darhol, sinxron chaqiriladi (bu ko'p odam bilmaydigan muhim nuqta):

console.log('1');
new Promise((resolve) => {
  console.log('2'); // BU DARHOL, SINXRON chiqadi!
  resolve();
});
console.log('3');
// natija: 1, 2, 3 — Promise konstruktorining o'zi async EMAS

Faqat .then() ichidagi callback — asinxron (microtask). Konstruktor ichidagi kod — oddiy sinxron kod.


3. resolve va reject — bular aslida nima?

resolve va reject — V8 tomonidan maxsus yaratilgan ikkita funksiya, ular closure orqali shu aniq Promise obyektiga bog'langan. Soddalashtirilgan tasavvur:

function createResolve(promiseInternal) {
  return function resolve(value) {
    if (promiseInternal.state !== 'pending') return; // qotib qolgan, chiqib ket
    promiseInternal.state = 'fulfilled';
    promiseInternal.result = value;
    // navbatdagi barcha .then() callback'larni MICROTASK sifatida rejalashtir
    scheduleReactions(promiseInternal.fulfillReactions, value);
  };
}

resolve(value) chaqirilganda, u darhol callback'larni chaqirmaydi — u faqat holatni o'zgartiradi va reaksiyalarni (agar bor bo'lsa) Microtask Queue'ga rejalashtiradi.


4. .then() — qadam-baqadam, ichkarida nima bo'ladi

Bu — eng muhim qism. .then(onFulfilled, onRejected) chaqirilganda uchta narsa sodir bo'ladi:

  1. .then() — DARHOL yangi Promise qaytaradi (holati hali qanday bo'lishidan qat'i nazar):
const p1 = new Promise((resolve) => resolve(10));
const p2 = p1.then((val) => val * 2); // p2 — YANGI, alohida Promise
  1. Agar p1 hali pending bo'lsa — onFulfilled funksiya p1ning [[PromiseFulfillReactions]] ro'yxatiga qo'shib qo'yiladi (hozircha chaqirilmaydi, faqat "ro'yxatga yozib qo'yiladi").

  2. Agar p1 allaqachon fulfilled bo'lsa (yoki resolve chaqirilgan payt) — reaksiya darhol Microtask Queue'ga joylashtiriladi (lekin baribir sinxron emas — bajarilishi Call Stack bo'shashini kutadi).

Diagram bilan ko'rsataman:

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve(5), 1000);
});

p1.then((val) => console.log(val * 2)); // (A)
p1.then((val) => console.log(val * 3)); // (B)
p1 [[PromiseFulfillReactions]]: [ ]
       │
       ▼  .then() chaqirilganda (A)
p1 [[PromiseFulfillReactions]]: [ reactionA ]
       │
       ▼  .then() chaqirilganda (B)
p1 [[PromiseFulfillReactions]]: [ reactionA, reactionB ]
       │
       ▼  1000ms o'tib, resolve(5) chaqiriladi
p1 [[PromiseState]] = "fulfilled", [[PromiseResult]] = 5
       │
       ▼  ro'yxatdagi HAR BIR reaksiya Microtask Queue'ga qo'shiladi
Microtask Queue: [ reactionA(5), reactionB(5) ]
       │
       ▼  Event Loop, Call Stack bo'shagach, navbat bilan bajaradi
console.log(10)   console.log(15)

Shuning uchun bitta Promise'ga bir nechta .then() ulash mumkin — ularning barchasi bitta ro'yxatda saqlanadi va Promise fulfilled/rejected bo'lganda hammasi birdek ishga tushiriladi.


5. Nega .then() natijasi — YANGI Promise (Chaining sirlarining kaliti)

Promise.resolve(1)
  .then((val) => val + 1)   // p2
  .then((val) => val + 1)   // p3
  .then((val) => console.log(val)); // 3

Har bir .then() — yangi Promise yaratadi, va bu yangi Promise'ning holati onFulfilled funksiyaning qaytargan qiymatiga bog'liq:

onFulfilled MUMKIN QAYTARSIN:
  - oddiy qiymat  → yangi Promise darhol shu qiymat bilan "fulfilled" bo'ladi
  - Promise       → yangi Promise O'SHA Promise HAL BO'LGUNCHA "pending" qoladi,
                     keyin uning holatini "meros qilib oladi" (bu — Resolution Procedure)
  - xato tashlasa (throw) → yangi Promise "rejected" bo'ladi

Ikkinchi holat — eng muhim va murakkab qism, chunki aynan shu orqali ichma-ich Promise'lar avtomatik "yozilib" (flatten) ketadi:

function getUser() {
  return new Promise((resolve) => setTimeout(() => resolve({ id: 1 }), 1000));
}

function getOrders(user) {
  return new Promise((resolve) => setTimeout(() => resolve(['order1']), 1000));
}

getUser()
  .then((user) => getOrders(user)) // BU YERDA Promise qaytarilyapti!
  .then((orders) => console.log(orders));

Ikkinchi .then() — ordersni to'g'ridan-to'g'ri massiv sifatida oladi, Promise<Promise<array>> emas. Bu — Promise Resolution Procedure (spec'da [[Resolve]](value) deb ataladi) tufayli:

  1. onFulfilled qaytargan qiymat — Promise-mi (yoki "thenable", ya'ni .then metodiga ega har qanday obyekt)-mi tekshiriladi
  2. Agar HA bo'lsa:
    • yangi Promise (p_next) holati "pending"da QOLDIRILADI
    • ichki qaytarilgan Promise'ning O'ZIGA .then() ulanadi (ichki, avtomatik)
    • ichki Promise hal bo'lganda, UNING natijasi p_next'ga "uzatiladi" (ya'ni p_next ENDI resolve/reject qilinadi)
  3. Agar YO'Q bo'lsa (oddiy qiymat):
    • p_next darhol shu qiymat bilan fulfilled qilinadi

Shu mexanizm tufayli, nechta Promise ichma-ich qaytarilishidan qat'i nazar, zanjir hech qachon Promise<Promise<Promise<...>>> shaklida "yig'ilib" qolmaydi — har doim "tekislanadi" (flatten).


6. .catch() va xato tarqalishi (propagation)

.catch(fn) — aslida shunchaki .then(undefined, fn) uchun qisqartma (syntactic sugar). Xato zanjir bo'ylab keyingi .catch() yoki ikkinchi argumentli .then() topilguncha "sakrab" o'tadi:

Promise.reject('xato')
  .then((val) => console.log('bu ishlamaydi'))   // o'tkazib yuboriladi (onRejected yo'q)
  .then((val) => console.log("bu ham o'tkaziladi")) // yana o'tkaziladi
  .catch((err) => console.log('ushlandi:', err));  // faqat SHU YERDA ushlanadi

Ichkarida bu qanday ishlaydi: har bir .then() — agar onRejected argumenti berilmagan bo'lsa, V8 default reaksiya qo'yadi — bu reaksiya shunchaki qabul qilingan xatoni qayta tashlaydi (re-throw), ya'ni keyingi Promise'ni ham rejected qilib, sabab bir xil qoladi. Shu tariqa xato .catch() topilguncha zanjir bo'ylab "oqib" boradi.


7. Nega .then() ichidagi kod — MICROTASK (V8'dagi asosiy sabab)

resolve(value) chaqirilganda, reaksiyalar Call Stack'ga darhol qo'yilmaydi — chunki bu ECMAScript spesifikatsiyasida qat'iy talab qilingan: Promise reaksiyalari har doim PromiseJobs (V8'da bu Microtask deb amalga oshirilgan) sifatida ishga tushishi kerak, hech qachon sinxron emas. Sabab — izchillik:

function test(promise) {
  promise.then(() => console.log('ichida'));
  console.log('tashqarida');
}

test(Promise.resolve()); // agar allaqachon resolved bo'lsa ham...

Agar resolve() allaqachon chaqirilgan bo'lsa ham, .then()ning callback'i hech qachon darhol chaqirilmaydi — u har doim, hatto Promise darhol tayyor bo'lsa ham, navbatdagi microtask sifatida ishlaydi. Shu tufayli natija har doim: tashqarida, keyin ichida — kod qachon .then() chaqirilganidan qat'i nazar, tartib bashorat qilinadigan (predictable) bo'lib qoladi. Aks holda, ba'zida sinxron, ba'zida asinxron ishlaydigan funksiya — dasturchi uchun eng xavfli holatlardan biri bo'lardi ("Zalgo" muammosi deb ataladi).


Xulosa diagram — hammasi birga

new Promise(executor)
        │
        ▼ executor DARHOL, sinxron chaqiriladi
        │
   resolve(v) yoki reject(e) chaqirilishini kutamiz
        │
        ▼
[[PromiseState]]: pending → fulfilled/rejected (BIR MARTA, qaytmas)
        │
        ▼
Ro'yxatdagi barcha .then()/.catch() reaksiyalari
        │
        ▼
Har biri MICROTASK QUEUE'ga qo'yiladi (V8 ichida, spec darajasida)
        │
        ▼
Event Loop, Call Stack bo'shagach, navbat bilan bajaradi
        │
        ▼
Har bir .then() — YANGI Promise qaytaradi → agar qaytgan qiymat Promise bo'lsa,
Resolution Procedure orqali "tekislanadi" (flatten)