Từ Zero đến Principal Frontend Engineer (P11: Thế Giới Ngầm Của Event Driven)
Chào mừng các bạn đến với hội những người thích nghe lén, hay còn gọi là Event Driven Architecture (EDA)! Hôm nay, chúng ta sẽ không code theo kiểu "anh gọi, em trả lời" truyền thống nữa, mà sẽ chuyển sang mode "tôi nói một câu, cả hệ thống xì xào bàn tán" 🕵️♂️
Phần 1: Thằng "Gửi Thư" và Đám "Nghe Lén"
Cuộc sống trước khi có Event Driven:
Anh Component A: "Ê Component B, mày fetch cái data này cho tao!"
Component B: "Đợi tí... đang làm... xong rồi! Đây!"
Anh A: "Good! Giờ Component C, mày render cái này đi!"
Component C: "Dạ! Đã render xong!"
Anh A: "Cảm ơn các em. Giờ anh mệt quá, phải đi ngủ."
→ Kiểu này giống như ông chủ phải đứng chỉ đạo từng tí một. Mệt chết!
Cuộc sống sau khi có Event Driven:
Anh Component A (hét toáng lên): "USER_VỪA_LOGIN_NÈ!!!"
*Lập tức:*
- Component B: "À user login rồi? Tao fetch profile liền!"
- Component C: "Để tao update cái header cho đẹp!"
- Component D: "Tao gửi analytics event cho bọn marketing!"
- Component E: "Mấy đứa tính xong chưa? Tao render liền đây!"
Điều thần kỳ: Anh Component A KHÔNG CẦN BIẾT có bao nhiêu đứa đang nghe, và chúng làm gì. A chỉ biết hét thôi!
Phần 2: Mấy Kiểu "Rỉ Tai" Trong Giới Frontend
1. Emitter Cổ Điển (Event Emitter Pattern)
// Khai báo một thằng chuyên đi "buôn chuyện"
const gossipMaster = new EventEmitter();
// Đám đang thích ngồi lê đôi mách
gossipMaster.on('CÓ_TIN_NÓNG', (data) => {
console.log(`Ôi trời ơi, ${data.user} vừa làm ${data.action} kìa!`);
console.log(`Chuyền tiếp cho 5 người bạn...`);
});
// Thằng đi bắn tin
gossipMaster.emit('CÓ_TIN_NÓNG', {
user: 'Thằng Hưng',
action: 'đẩy code lên production lúc 5h chiều thứ 6'
});
// Output: Cả văn phòng hoảng loạn 😱
Đặc điểm: Giống như bà hàng xóm có loa phóng thanh. Nói là cả xóm nghe.
2. Pub/Sub (Publisher/Subscriber) - Phiên Bản Cao Cấp Hơn
// Tưởng tượng có một cái "Bảng Tin Facebook" cho code
const messageBoard = {
// Danh sách những kẻ "stalker" theo dõi từng chủ đề
stalkers: {
'USER_ACTION': [stalker1, stalker2],
'SYSTEM_ALERT': [stalker3],
'DATA_UPDATED': [stalker4, stalker5, stalker6]
},
subscribe: function(topic, stalkerFunction) {
// "Ê mày, có tin gì về chủ đề này thì gọi tao nha"
if (!this.stalkers[topic]) this.stalkers[topic] = [];
this.stalkers[topic].push(stalkerFunction);
},
publish: function(topic, gossip) {
// "ALO ALO, CÓ TIN NÓNG ĐÂY!"
if (this.stalkers[topic]) {
this.stalkers[topic].forEach(stalker => {
stalker(gossip); // Gọi cho từng đứa đang "stalk"
});
}
}
}
// Đăng ký nghe lén
messageBoard.subscribe('USER_ACTION', (info) => {
console.log(`[Thằng ham click] thấy user click: ${info.button}`);
});
messageBoard.subscribe('USER_ACTION', (info) => {
console.log(`[Thằng gửi analytics] báo cáo: ${JSON.stringify(info)}`);
});
// Bắn tin
messageBoard.publish('USER_ACTION', {
button: 'BUY_NOW',
price: 999999,
user: 'Ông già Noel'
});
Phần 3: Real Case - Khi Event Driven Cứu Cả Dự Án
Scenario: Checkout Flow Siêu Phức Tạp
Bạn có:
CartComponent(giỏ hàng)ShippingComponent(vận chuyển)PaymentComponent(thanh toán)DiscountComponent(mã giảm giá)RecommendationComponent(gợi ý thêm)
Với Event Driven:
// Khi user thêm sản phẩm vào giỏ
eventBus.publish('CART_ITEM_ADDED', {
productId: 'IPHONE_99',
quantity: 1,
price: 29999999
});
// Và magic xảy ra...
// 1. CartComponent tự update số lượng
// 2. DiscountComponent check xem có được free ship không
// 3. RecommendationComponent: "Mua thêm case đi anh!"
// 4. Analytics gửi event: "Thằng này có máu shopping"
// TẤT CẢ DIỄN RA TỰ ĐỘNG, KHÔNG AI GỌI AI!
Phần 4: Mấy Cái Bẫy Khi "Buôn Chuyện" Quá Nhiều
Bẫy số 1: "Tôi Nghe Không Rõ" (Event Loss)
// THẢM HỌA: Event bị thất lạc
eventBus.publish('USER_PAID', { orderId: '123' });
// Nhưng PaymentComponent đang loading chưa subscribe kịp...
// → Mất tiền như chơi 💸
Giải pháp: Dùng event sourcing (ghi lại toàn bộ lịch sử), hoặc persistent event bus.
Bẫy số 2: "Vòng Lặp Vô Tận" (Event Loop)
eventBus.on('A', () => { eventBus.emit('B'); });
eventBus.on('B', () => { eventBus.emit('A'); });
// A gọi B, B gọi A, A gọi B, B gọi A...
// Browser: "Chào thua, crash đây!" 💥
Bẫy số 3: "Ai Nghe Gì Tao Không Biết" (Debug Hell)
Lỗi: Discount tính sai
Tìm nguyên nhân:
- Event 'PRICE_UPDATED' được emit từ 5 chỗ
- Có 8 component subscribe event đó
- Mỗi component lại emit event khác...
→ Debug như đi tìm kim đáy biển 🧭
Phần 5: Mấy Ông Lớn Dùng Event Driven
Redux? Thực Ra Là Event Driven Đấy!
// Action chính là Event
dispatch({ type: 'USER_LOGIN', payload: userData });
// Reducer là Subscriber
function rootReducer(state, action) {
switch(action.type) { // Lắng nghe event type
case 'USER_LOGIN': return handleLogin(state, action);
// ... nghe thêm nhiều event khác
}
}
RxJS - Vua Của Các Vua Event Driven
// RxJS là Event Driven trên steroid
fromEvent(document, 'click') // Event source
.pipe(
debounceTime(300), // "Đợi xíu, đừng vội"
filter(click => click.target.matches('.buy-btn')), // Lọc
map(click => ({ // Biến đổi
productId: click.target.dataset.id,
timestamp: Date.now()
})),
switchMap(data => // Gọi API
fetch('/api/purchase', { method: 'POST', body: JSON.stringify(data) })
)
)
.subscribe(response => { // Cuối cùng xử lý
console.log('Mua thành công!');
});
// Đọc xong tự hỏi: "Tôi vừa đọc cái gì vậy?" 🤯
Phần 6: Khi Nào Thì Nên "Buôn Chuyện"?
HÃY DÙNG KHI:
✅ Micro Frontends - Các app độc lập cần nói chuyện với nhau
✅ Real-time apps - Chat, notification, dashboard
✅ Complex UI flows - Form wizard, multi-step process
✅ Decoupling - Muốn component không biết nhau tồn tại
✅ Plugin architecture - Cho phép mở rộng tự động
ĐỪNG DÙNG KHI:
❌ App nhỏ - Giết muỗi bằng tên lửa
❌ Flow đơn giản - Props drilling là quá đủ
❌ Team mới - Chưa hiểu đã dùng = bom nổ chậm
❌ Performance cực kỳ quan trọng - Event có overhead
Phần 7: Code Thực Tế - Xây "Bảng Tin" Của Riêng Bạn
// eventBus.js - Đơn giản mà mạnh mẽ
class EventBus {
constructor() {
this.listeners = new Map();
}
// Đăng ký nghe lén
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
// Trả về function để unsubscribe (bỏ nghe lén)
return () => this.off(event, callback);
}
// Bỏ nghe lén
off(event, callback) {
const callbacks = this.listeners.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) callbacks.splice(index, 1);
}
}
// Bắn tin (có thể delay nếu muốn)
emit(event, data, delay = 0) {
const callbacks = this.listeners.get(event);
if (callbacks) {
// Async để không block main thread
setTimeout(() => {
callbacks.forEach(cb => {
try {
cb(data);
} catch (err) {
console.error(`Event ${event} error:`, err);
}
});
}, delay);
}
}
// Nghe 1 lần rồi thôi
once(event, callback) {
const onceCallback = (data) => {
callback(data);
this.off(event, onceCallback);
};
this.on(event, onceCallback);
}
}
// Sử dụng trong React
const eventBus = new EventBus();
// Component A - Thằng bắn tin
function AddToCartButton({ product }) {
const handleClick = () => {
eventBus.emit('CART_ADD', {
product,
user: currentUser,
timestamp: new Date()
});
};
return <button onClick={handleClick}>Mua Ngay</button>;
}
// Component B - Thằng nghe lén (không biết A tồn tại)
function CartNotification() {
const [count, setCount] = useState(0);
useEffect(() => {
// Subscribe
const unsubscribe = eventBus.on('CART_ADD', () => {
setCount(prev => prev + 1);
// Hiện notification
showToast('Đã thêm vào giỏ hàng!');
});
// Cleanup
return unsubscribe;
}, []);
return <div>Giỏ hàng: {count} sản phẩm</div>;
}
Phần 8: Best Practices - "Buôn Chuyện" Có Văn Hóa
1. Đặt tên event rõ ràng
// TỆ
eventBus.emit('update', data); // Update cái gì? Ai update?
// TỐT
eventBus.emit('USER_PROFILE_UPDATED', { userId, newData });
eventBus.emit('PAYMENT_SUCCEEDED', { orderId, amount });
2. Dùng TypeScript để không "nghe nhầm"
// Định nghĩa event types
type AppEvents = {
'USER_LOGIN': { userId: string; email: string };
'CART_UPDATED': { items: CartItem[]; total: number };
'NOTIFICATION_SHOW': { message: string; type: 'info' | 'error' };
};
// EventBus type-safe
class TypedEventBus {
emit<T extends keyof AppEvents>(event: T, data: AppEvents[T]) { /* ... */ }
on<T extends keyof AppEvents>(event: T, callback: (data: AppEvents[T]) => void) { /* ... */ }
}
// Giờ không thể emit sai data structure được nữa!
eventBus.emit('USER_LOGIN', { userId: '123' }); // ❌ thiếu email
3. Logging & Debugging
// Thêm debug mode
class DebugEventBus extends EventBus {
emit(event, data) {
console.log(`📢 [Event Fired] ${event}:`, data);
super.emit(event, data);
}
on(event, callback) {
console.log(`👂 [Listening] ${event}`);
return super.on(event, callback);
}
}
4. Error Boundaries cho Event Handlers
eventBus.on('IMPORTANT_EVENT', (data) => {
try {
// Xử lý logic
} catch (error) {
// KHÔNG ĐƯỢC để error này silent fail
console.error('Event handler failed:', error);
// Có thể emit error event
eventBus.emit('HANDLER_ERROR', { event: 'IMPORTANT_EVENT', error });
}
});
Kết Luận: Từ "Gọi Điện" Sang "Bắn Tin Nhóm"
Event Driven không phải bùa chú vạn năng, mà là một công cụ cực mạnh khi dùng đúng chỗ.
Hãy nhớ:
- Component giao tiếp qua event giống như dân văn phòng dùng Slack
- Mỗi người chỉ cần biết mình quan tâm channel nào
- Không cần biết ai đang online, ai đang làm gì
- Hệ thống trở nên linh hoạt và dễ mở rộng
Cuối cùng: Nếu app của bạn bắt đầu có cảm giác như một bà mẹ chồng phải quản lý đám con dâu (phải biết hết mọi thứ, điều khiển từng tí), thì đã đến lúc cho chúng nó tự "buôn chuyện" với nhau qua event driven rồi đó!
P/S: Nếu bạn code xong mà thấy các component tự động làm việc mà không cần bạn ra lệnh, xin chúc mừng - bạn vừa tạo ra một hệ thống có trí tuệ tập thể! Hoặc cũng có thể bạn vừa tạo ra một đống bug khó debug. 50/50 thôi! 😂
All Rights Reserved