Pub/Sub (Publish/Subscribe)
Pub/Sub Observer'dan qanday farqlanishi (vositachi Event Bus orqali decoupled modullar), qo'lda EventBus yozish (on/emit/off/once), emit'ning sinxron Call Stack zanjiri, queueMicrotask/setTimeout bilan asinxron qilish, va off() unutilsa memory leak.
79-dars
Pub/Sub (Publish/Subscribe)
Oldingi mavzuda aytganimizdek: Observer'da Subject observerlarni to'g'ridan-to'g'ri biladi (subject.subscribe(fn) — bu bevosita, bitta obyektga bog'langan chaqiruv). Pub/Sub'da esa orada alohida vositachi qatlam — odatda Event Bus yoki Event Emitter deb ataladigan narsa — bor, va Publisher bilan Subscriber bir-birini umuman bilmaydi, faqat umumiy "event nomi" (string) orqali bog'lanadi.
Observer: Subject ──(to'g'ridan-to'g'ri referens)──> Observer1, Observer2
Pub/Sub: Publisher ──"event nomi"──> [Event Bus] ──"event nomi"──> Subscriber1, Subscriber2
Bu farq amalda nima beradi — Publisher kod yozganda, u qaysi modul, qaysi komponent bu eventni tinglayotganini hech qachon bilmaydi va bilishi ham shart emas. Bu — modullarni butunlay bog'liqsiz (decoupled) qilib qo'yadi.
1. O'zimiz Event Bus yozamiz — qadam-baqadam
1-qadam: Asosiy struktura — nomlar bo'yicha guruhlangan callback'lar
class EventBus {
constructor() {
// Har bir event nomi uchun callback'lar RO'YXATI (massiv) saqlanadi
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, data) {
if (!this.events[eventName]) return; // hech kim tinglamayapti
this.events[eventName].forEach(callback => callback(data));
}
off(eventName, callback) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
}
Diqqat qiling: this.events — bu oddiy obyekt, key sifatida string (event nomi), qiymat sifatida funksiyalar massivi. Bu — Data Structure jihatidan juda oddiy, lekin aynan shu soddalik Pub/Sub'ning kuchi.
2-qadam: Ishlatib ko'ramiz — butunlay bog'liqsiz modullar
const bus = new EventBus();
// ------ user.js fayli (bu haqida hech narsa bilmaydi kim tinglayotganini) ------
function loginUser(username) {
console.log(`${username} tizimga kirdi`);
bus.emit('user:login', { username, time: Date.now() });
}
// ------ analytics.js fayli (bu haqida hech narsa bilmaydi kim emit qilganini) ------
bus.on('user:login', (data) => {
console.log(`[Analytics] Login qayd etildi: ${data.username}`);
});
// ------ notification.js fayli ------
bus.on('user:login', (data) => {
console.log(`[Notification] Xush kelibsiz, ${data.username}!`);
});
// ------ main.js ------
loginUser('Aziz');
// Aziz tizimga kirdi
// [Analytics] Login qayd etildi: Aziz
// [Notification] Xush kelibsiz, Aziz!
Bu yerda user.js, analytics.js, notification.js — hech biri bir-birini import qilmagan! Ular faqat busga (umumiy Event Bus instansiyasiga) va umumiy "user:login" degan string nomga kelishib olgan, xolos. Bu — Observer'dagi subject.subscribe(fn)dan farqli, chunki u yerda Subject o'zi notify qilardi, bu yerda esa istalgan modul bus.emit(...) orqali xabar yubora oladi, va bu modulning o'zi Subject bo'lishi shart emas.
2. once — bir martalik tinglash (amaliy qo'shimcha)
Real Event Bus'larda ko'pincha kerak bo'ladigan funksiya — faqat bir marta ishlab, keyin avtomatik o'chadigan listener:
class EventBus {
// ... oldingi metodlar
once(eventName, callback) {
const wrapper = (data) => {
callback(data);
this.off(eventName, wrapper); // o'zini o'zi o'chiradi
};
this.on(eventName, wrapper);
}
}
Bu yerda closure yana ishga tushyapti: wrapper funksiyasi o'zining tashqi scope'idagi eventName, callback, va thisga (agar arrow function bo'lsa) yopishib qoladi, va chaqirilganda o'zini (wrapperning o'zini) off orqali ro'yxatdan olib tashlaydi.
3. V8/runtime darajasida nima sodir bo'ladi — sinxron chaqiruv zanjiri
Bu yerda muhim, ko'pincha e'tibordan chetda qoladigan nozik nuqta bor: yuqoridagi oddiy emit implementatsiyasi — butunlay sinxron.
emit(eventName, data) {
this.events[eventName].forEach(callback => callback(data));
}
Bu degani — avvalda o'rgangan Call Stack tushunchasiga qaytsak: emit('user:login', data) chaqirilganda, bitta stack frame ichida, ketma-ket, barcha subscriber'lar darhol, birin-ketin chaqiriladi. Agar analytics.jsdagi callback ichida og'ir, uzoq davom etadigan sinxron kod bo'lsa (masalan, katta massiv ustida for loop), bu butun call stack'ni band qilib turadi, va notification.jsdagi callback ham, undan keyingi har qanday kod ham — kutishga majbur, chunki JS single-threaded.
Call Stack (emit paytida):
emit('user:login')
→ forEach
→ analytics callback (agar sekin bo'lsa - HAMMA shu yerda kutadi)
→ notification callback (faqat yuqoridagisi tugagach ishga tushadi)
4. Buni qanday yumshatish mumkin — mikrotask/makrotask bilan bog'liqlik
Agar subscriber'lar bir-biridan mustaqil ravishda, bloklamasdan ishlashi kerak bo'lsa, emitni asinxron qilish mumkin — bu yerda avval o'rgangan Event Loop, Microtask/Macrotask tushunchalari amaliy qo'llaniladi:
emit(eventName, data) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(callback => {
queueMicrotask(() => callback(data));
// yoki: setTimeout(() => callback(data), 0);
});
}
queueMicrotask ishlatilsa — har bir callback alohida microtask sifatida navbatga qo'yiladi, emit funksiyasining o'zi darhol tugaydi (bloklamaydi), lekin barcha callback'lar hali ham joriy sinxron kod tugagach, lekin keyingi macrotask (masalan, setTimeout)dan oldin ishga tushadi (Microtask vs Macrotask navbat tartibini eslang). setTimeout(..., 0) ishlatilsa — bu macrotask navbatiga tushadi, ya'ni barcha joriy va navbatdagi microtask'lardan keyin ishlaydi.
Bu — real-world kutubxonalarda (masalan, Node.js'ning ichki EventEmitteri) nega ba'zan sinxron, ba'zan asinxron xatti-tutish tanlanishi mumkinligining sababi — bu dizayn qarori, va Pub/Sub yozuvchisi buni ongli ravishda tanlashi kerak.
5. Xotira boshqaruvi — bu yerda ham memory leak xavfi bor
Xuddi Observer'da bo'lgani kabi, Pub/Sub'da ham eng keng tarqalgan xato — off() chaqirishni unutish:
class ChatWidget {
constructor(bus) {
this.bus = bus;
this.handleMessage = (msg) => this.render(msg);
bus.on('new-message', this.handleMessage);
}
destroy() {
this.bus.off('new-message', this.handleMessage); // BU QATOR SHART!
}
}
EventBusning this.events obyekti odatda dastur umri davomida yashaydigan (uzoq umr ko'radigan root'ga yaqin) narsa. Agar ChatWidget DOM'dan olib tashlansa-yu, lekin off() chaqirilmasa, bus.events['new-message'] massivi hali ham this.handleMessagega (demak, butun ChatWidget instansiyasiga, chunki bu arrow function thisni closure orqali ushlab turibdi) referens saqlab qoladi — reachability qoidasi bo'yicha, GC bu obyektni hech qachon tozalay olmaydi.
Xulosa jadvali — Observer vs Pub/Sub
| Observer | Pub/Sub | |
|---|---|---|
| Bog'lanish | To'g'ridan-to'g'ri referens (subject.subscribe) |
Vositachi (Event Bus) orqali, faqat nom bilan |
| Publisher subscriber'ni biladimi | Ha, ular bir xil "ekotizim"da | Yo'q, umuman bilmaydi |
| Tipik ishlatilish | Bitta obyekt holatini kuzatish (masalan, state) | Modullararo, butun ilova bo'ylab kommunikatsiya |
| Real misol | addEventListener (DOM) |
Redux middleware, global event bus, Node EventEmitter |