Series: JavaScript Basics Lesson 47

Callback

Callback nima ekani (sinxron va asinxron), continuation-passing style, Callback Hell (Pyramid of Doom) muammosi, Node.js'dagi error-first callback konventsiyasi, va closure orqali callback V8 darajasida qanday ishlashi.

47-dars

Callback

Callback — bu boshqa funksiyaga argument sifatida uzatilgan, keyinroq chaqirilishi kutilayotgan funksiya. Hech qanday maxsus sintaksis emas — oddiy funksiya, faqat uni "keyin ishlat" degan maqsadda boshqa funksiyaga beryapsiz.

function greet(name, callback) {
  console.log('Salom, ' + name);
  callback();
}

greet('Ali', () => {
  console.log('Bu — callback, keyinroq chaqirildi');
});

Bu yerda hech qanday async yo'q — callback() darhol, sinxron chaqirilyapti. Muhim tushuncha: "callback" — bu asinxronlik bilan bog'liq atama emas. Callback — shunchaki "funksiyani funksiyaga uzatish" degan umumiy dizayn pattern. map, filter, addEventListener — bularning barchasida callback bor, lekin ularning ba'zisi sinxron (map), ba'zisi asinxron (setTimeout).


1. Asinxron callback

Web APIlar (oldingi mavzuda ko'rganimiz) natijani qaytarish uchun faqat callback orqali ishlashi mumkin — Promise ixtiro qilinmasdan oldin, JS'da natija qaytarishning yagona usuli shu edi:

setTimeout(() => {
  console.log('2 sekunddan keyin ishga tushdi');
}, 2000);

Bu yerda () => {...} — asinxron callback: u darhol emas, balki Web API (Timer) o'z ishini tugatgach, Event Loop orqali chaqiriladi (oldingi mavzularda batafsil ko'rganimiz mexanizm).


2. Nega Callback

JS'da return orqali qiymat qaytarish faqat sinxron ishlaydi:

function getData() {
  // 2 sekund kutib, keyin qaytarish MUMKIN EMAS
  return 'data'; // bu darhol qaytadi, kutish yo'q
}

Agar funksiya ichida server javobini "kutish" kerak bo'lsa (masalan, tarmoq so'rovi), funksiya darhol tugaydi (chunki bitta thread, blocking qilib bo'lmaydi — birinchi mavzuda ko'rganimiz), natija esa hali tayyor emas. Yechim: natijani return orqali emas, balki kelajakda chaqiriladigan funksiya (callback) orqali "yetkazish":

function getData(callback) {
  setTimeout(() => {
    callback('data keldi'); // natija TAYYOR bo'lganda, callback CHAQIRILADI
  }, 2000);
}

getData((result) => {
  console.log(result); // 2 sekunddan keyin: "data keldi"
});

Bu pattern — "continuation-passing style" deb ataladi: funksiya natijani qaytarmaydi, balki "keyin nima qilish kerakligini" (continuation) callback sifatida qabul qiladi va o'zi chaqiradi.


3. Muammo — "Callback Hell" (Pyramid of Doom)

Agar bir nechta asinxron operatsiya ketma-ket, bir-biriga bog'liq bo'lsa (masalan: foydalanuvchini top → uning buyurtmalarini top → buyurtma tafsilotini top), callback'lar ichma-ich joylashadi:

getUser(userId, (user) => {
  getOrders(user.id, (orders) => {
    getOrderDetails(orders[0].id, (details) => {
      getShippingInfo(details.id, (shipping) => {
        console.log(shipping); // 4 daraja chuqurlik!
        // xato bo'lsa-chi? Har bir bosqichda alohida tekshirish kerak
      });
    });
  });
});

Bu — Callback Hell deb ataladi. Muammolar:

  1. O'qish qiyin — kod o'ngga qarab "uchburchak" shaklida o'sadi
  2. Xatolarni boshqarish og'ir — har bir callback'da alohida if (err) tekshirish kerak (Node.js konventsiyasi — "error-first callback")
  3. Parallel bajarish qiyin — agar 3 ta so'rovni bir vaqtda yubormoqchi bo'lsangiz, callback'lar bilan buni to'g'ri boshqarish (masalan, uchalasi tugaganda bitta ishni bajarish) juda ko'p qo'lda yozilgan hisoblagich talab qiladi
  4. Callback ikki marta chaqirilishi mumkin yoki umuman chaqirilmasligi mumkin — hech qanday kafolat yo'q, bu — dizaynning tabiiy zaifligi

4. Error-first callback — Node.js konventsiyasi

Node.js'da standart qabul qilingan usul — callback'ning birinchi argumenti har doim xato uchun ajratiladi:

fs.readFile('file.txt', (err, data) => {
  if (err) {
    console.error('Xato:', err);
    return;
  }
  console.log(data);
});

Bu — konventsiya, til darajasidagi majburiyat emas. Har bir dasturchi buni qo'lda, har safar tekshirishi kerak — birontasini unutib qo'ysangiz, xato "yutilib" ketadi, hech qanday ogohlantirishsiz.


5. V8/Engine darajasida — callback qanday "jismonan" ishlaydi

Bu joyda hech qanday yangi mexanizm yo'q — callback, aslida, faqat oddiy funksiya obyekti, u heap'da saqlanadi:

function getData(callback) {
  setTimeout(() => {
    callback('natija');
  }, 1000);
}
  1. getData chaqirilganda, callback argumenti (funksiya) heap'da saqlanadi.
  2. setTimeout ichidagi arrow function — bu closure: u callbackni o'z Lexical Environmentida ushlab turadi (garchi tashqi getData funksiyasi Call Stack'dan chiqib ketgan bo'lsa ham!)
  3. setTimeoutning o'zi — Web API'ga topshiriladi
  4. 1000ms o'tgach, Web API arrow function'ni Macrotask Queue'ga qo'yadi
  5. Event Loop uni Call Stack'ga olib chiqadi, bajaradi — va aynan shu yerda, closure orqali saqlangan callback chaqiriladi

Ya'ni: callback — Event Loop yoki V8'ning maxsus "tushunchasi" emas. U — oddiygina, closure orqali xotirada ushlab turilgan, keyinroq chaqiriladigan oddiy funksiya. Butun "sehr" — closure (funksiya o'z yaratilgan muhitini eslab qolishi) va Event Loop (callback'ni to'g'ri vaqtda Call Stack'ga qo'yish) mexanizmlarining birgalikda ishlashida.


Xulosa — nega Promise kerak bo'lgan?

Callback Hell muammosini yechish uchun dasturchilar avval turli "workaround"lar (masalan, named function'lar, async.js kutubxonasi) ishlatishgan, lekin bular tub muammoni (xatoni boshqarish, tartibsizlik, kafolat yo'qligi) hal qilmagan. ES6 (2015) da shu muammolarni til darajasida, standart sifatida hal qilish uchun — Promise kiritildi: u callback'ni strukturaviy, kafolatlangan, zanjir shaklida yozsa bo'ladigan obyektga o'raydi.