Event Bus Trong JavaScript và TypeScript 🚍
Event Bus là một trung tâm publish/subscribe đơn giản giúp các module có thể giao tiếp mà không cần tham chiếu trực tiếp đến nhau. Trong bài viết này, chúng ta sẽ tìm hiểu cách sử dụng Event Bus trong JavaScript và TypeScript, những lợi ích, cũng như một số mẫu sử dụng thực tế.
Mục Lục
- Tại sao nên sử dụng Event Bus?
- Triển khai JavaScript tối giản và nhanh chóng
- Event Bus với TypeScript
- Mẫu sử dụng trong React
- Node.js: Bạn đã có một Event Bus
- Mẹo và cạm bẫy
- Tóm tắt nhanh
Tại sao nên sử dụng Event Bus?
Sử dụng Event Bus mang lại nhiều lợi ích, bao gồm:
- Tách biệt: Các nhà xuất bản không cần phải nhập các nhà đăng ký, giúp giảm độ phụ thuộc giữa các module.
- Giao tiếp có thể tái sử dụng: Một tín hiệu có thể dẫn đến nhiều phản ứng độc lập.
- Rất hữu ích cho: các tín hiệu xác thực, các thông báo, phân tích, và phối hợp giữa các tính năng.
Lưu ý: Nên sử dụng bus một cách tiết kiệm. Đối với các ứng dụng lớn, hãy ưu tiên sử dụng một trình quản lý trạng thái có cấu trúc như Redux, Zustand, hoặc Vuex cho trạng thái chính.
Triển khai JavaScript tối giản và nhanh chóng
Dưới đây là một triển khai đơn giản cho Event Bus:
javascript
// eventBus.js
export class EventBus {
constructor() { this.map = Object.create(null) }
on(event, cb) {
(this.map[event] ||= new Set()).add(cb)
return () => this.off(event, cb) // hàm trợ giúp hủy đăng ký
}
once(event, cb) {
const off = this.on(event, (p) => { off(); cb(p) })
return off
}
off(event, cb) {
const set = this.map[event]; if (!set) return
set.delete(cb); if (set.size === 0) delete this.map[event]
}
emit(event, payload) {
const call = (set) => set && set.forEach(fn => fn(payload))
call(this.map[event])
const star = event.split(':')[0] + ':*'
call(this.map[star])
}
}
// Sử dụng
export const bus = new EventBus()
const off = bus.on('user:login', (u) => console.log('Xin chào', u.name))
bus.once('user:login', () => console.log('Thông báo chỉ hiển thị một lần'))
bus.emit('user:login', { name: 'Alice' })
// sau đó
off()
Tại sao phiên bản này?
Set
tránh việc trùng lặp,once
tự động dọn dẹp, kênh wildcard"domain:*"
nhẹ nhàng.
Event Bus với TypeScript (hợp đồng an toàn)
Dưới đây là cách triển khai Event Bus với TypeScript:
typescript
// typedBus.ts
type Events = {
'user:login': { name: string }
'user:logout': void
'chat:message': { text: string; from: string }
'user:*': unknown // kênh wildcard (tùy chọn)
}
export class TypedBus<E extends Record<string, any>> {
private map: { [K in keyof E]?: Set<(p: E[K]) => void> } = {};
on<K extends keyof E>(event: K, cb: (payload: E[K]) => void) {
(this.map[event] ||= new Set()).add(cb)
return () => this.off(event, cb)
}
once<K extends keyof E>(event: K, cb: (payload: E[K]) => void) {
const off = this.on(event, (p) => { off(); cb(p) })
return off
}
off<K extends keyof E>(event: K, cb: (payload: E[K]) => void) {
const set = this.map[event]; if (!set) return
set.delete(cb); if (set.size === 0) delete this.map[event]
}
emit<K extends keyof E>(event: K, payload: E[K]) {
const call = (set?: Set<(p: any) => void>) => set?.forEach(fn => fn(payload))
call(this.map[event])
const star = (String(event).split(':')[0] + ':*') as keyof E
call(this.map[star])
}
}
export const bus = new TypedBus<Events>()
Lợi ích của hợp đồng: Tự động hoàn thành cho tên sự kiện & hình dạng payload; an toàn tại thời điểm biên dịch.
Mẫu sử dụng trong React
A. Instance bus toàn cầu (ứng dụng đơn giản)
javascript
// bus.ts
export const bus = new EventBus()
// Navbar.tsx
useEffect(() => bus.on('user:login', () => setOpen(true)), [])
// LoginForm.tsx
const submit = () => bus.emit('user:login', { name: 'Alice' })
B. Context + bus có phạm vi (tách biệt tính năng / khả năng thử nghiệm)
javascript
const BusContext = createContext<{
bus: EventBus
} | null>(null)
export function BusProvider({ children }) {
const [bus] = useState(() => new EventBus())
return <BusContext.Provider value={{ bus }}>{children}</BusContext.Provider>
}
export function useBus() {
const ctx = useContext(BusContext); if (!ctx) throw new Error('Không có BusProvider')
return ctx.bus
}
C. Dọn dẹp các đăng ký
javascript
useEffect(() => {
const off = bus.on('toast:show', setToast)
return off // tự động hủy đăng ký khi unmount
}, [])
Node.js: Bạn đã có một Event Bus
javascript
import { EventEmitter } from 'node:events'
const bus = new EventEmitter()
bus.on('ping', () => console.log('pong'))
bus.emit('ping')
Sử dụng EventEmitter
của Node cho mã máy chủ; đối với trình duyệt và thư viện chia sẻ, một bus tùy chỉnh nhỏ là đủ.
Mẹo và cạm bẫy
- Đặt tên sự kiện với namespace:
user:login
,cart:itemAdded
. - Cung cấp một tài liệu đăng ký (sự kiện & payload) để tránh sự không khớp.
- Tránh toàn cầu hóa: xem xét bus có phạm vi theo từng tính năng.
- Rò rỉ bộ nhớ: luôn hủy đăng ký khi unmount;
once
giúp giảm thiểu vấn đề này. - Ưu tiên trình quản lý trạng thái cho trạng thái chia sẻ; sử dụng bus cho tín hiệu.
Tóm tắt nhanh
Nhu cầu | Đề xuất |
---|---|
Tín hiệu toàn cục một lần (toast/auth) | Event bus |
Trạng thái chia sẻ phức tạp | Redux/Zustand/Context |
Payload kiểu và tự động hoàn thành | TypeScript TypedBus |
Chỉ cho phép một phản ứng | once |
Phát tín hiệu đến nhiều người nghe | emit thông thường hoặc wildcard |
Bài viết này nhằm mục đích giúp bạn hiểu rõ hơn về Event Bus trong JavaScript và TypeScript, từ đó áp dụng vào các dự án thực tế của mình. Hãy thử nghiệm và chia sẻ kinh nghiệm của bạn nhé!