本文通过代码实例和通俗解释,带你彻底理解JavaScript中两种核心设计模式的异同与应用场景
什么是发布订阅模式?
发布订阅模式(Pub/Sub) 是一种消息通信模式,它就像一家邮局:
- 发布者是寄信人
 - 订阅者是收信人
 - 事件中心是邮局系统
 
核心特点:
- 发布者和订阅者完全解耦,互不认识
 - 所有通信通过事件中心传递
 - 支持一对多的消息广播
 
// 邮局系统(事件中心)
class EventEmitter {
    constructor() {
        this.eventList = {} // 信箱集合
    }
    
    // 订阅事件(登记信箱)
    on(eventName, callBack) {
        if (!this.eventList[eventName]) this.eventList[eventName] = []
        this.eventList[eventName].push(callBack)
    }
    
    // 发布事件(投递信件)
    emit(eventName, ...args) {
        if (this.eventList[eventName]) {
            this.eventList[eventName].forEach(cb => cb(...args))
        }
    }
    
    // 取消订阅(注销信箱)
    off(eventName, callBack) {
        if (this.eventList[eventName]) {
            this.eventList[eventName] = this.eventList[eventName].filter(
                cb => cb !== callBack
            )
        }
    }
    
    // 单次订阅(临时信箱)
    once(eventName, callBack) {
        const wrapper = (...args) => {
            callBack(...args)
            this.off(eventName, wrapper) // 自动取消订阅
        }
        this.on(eventName, wrapper)
    }
}
// 使用示例
const postOffice = new EventEmitter()
// 订阅者A(长期订阅)
postOffice.on('news', (message) => {
    console.log(`A收到新闻: ${message}`)
})
// 订阅者B(只收一次)
postOffice.once('news', (message) => {
    console.log(`B收到新闻: ${message} (仅接收一次)`)
})
// 发布者发送消息
postOffice.emit('news', 'JavaScript新特性发布!') 
/* 输出:
   A收到新闻: JavaScript新特性发布!
   B收到新闻: JavaScript新特性发布! (仅接收一次)
*/
postOffice.emit('news', 'Vue 4.0 即将发布') 
/* 输出:
   A收到新闻: Vue 4.0 即将发布 (B不再接收)
*/
什么是观察者模式?
观察者模式(Observer) 就像班级里的老师和学生:
- 被观察者(Subject)是老师
 - 观察者(Observer)是学生
 - 老师直接管理学生名单
 
核心特点:
- 被观察者直接维护观察者列表
 - 状态变更时直接通知所有观察者
 - 被观察者知道观察者的存在
 
// 老师(被观察者)
class Teacher {
    constructor() {
        this.students = [] // 学生名单
        this.homework = ''
    }
    
    // 添加学生
    addStudent(student) {
        this.students.push(student)
    }
    
    // 布置作业
    assignHomework(task) {
        this.homework = task
        this.notifyStudents() // 通知所有学生
    }
    
    // 通知学生
    notifyStudents() {
        this.students.forEach(student => {
            student.receiveHomework(this.homework)
        })
    }
}
// 学生(观察者)
class Student {
    constructor(name) {
        this.name = name
        this.task = null
    }
    
    receiveHomework(task) {
        this.task = task
        console.log(`${this.name}收到作业: ${task}`)
    }
}
// 使用示例
const teacher = new Teacher()
const studentA = new Student('小明')
const studentB = new Student('小红')
// 老师登记学生
teacher.addStudent(studentA)
teacher.addStudent(studentB)
// 老师布置作业
teacher.assignHomework('完成原型链练习')
/* 输出:
   小明收到作业: 完成原型链练习
   小红收到作业: 完成原型链练习
*/
关键差异对比
| 特性 | 发布订阅模式 | 观察者模式 | 
|---|---|---|
| 通信方式 | 通过事件中心中转(邮局系统) | 直接通知(老师喊话) | 
| 关系 | 多对多(多个发件人/收件人) | 一对多(一个老师对多个学生) | 
| 耦合度 | 完全解耦(发件人不认识收件人) | 松耦合(老师知道学生存在) | 
| 灵活性 | 高(动态添加/移除订阅) | 中(需要维护观察者列表) | 
| 典型应用 | 全局事件总线、跨组件通信 | 响应式系统、状态管理 | 
实际应用场景
发布订阅应用:跨组件通信
// 创建全局事件中心(组件间邮局)
const eventBus = new EventEmitter()
// 搜索组件(发布者)
document.getElementById('search-input').addEventListener('input', (e) => {
    eventBus.emit('search', e.target.value) // 发布搜索事件
})
// 结果展示组件(订阅者)
eventBus.on('search', (keyword) => {
    fetch(`/api/search?q=${keyword}`)
        .then(res => res.json())
        .then(data => {
            document.getElementById('results').innerHTML = 
                data.map(item => `<div>${item.title}</div>`).join('')
        })
})
// 历史记录组件(订阅者)
eventBus.on('search', (keyword) => {
    if(keyword) {
        addToSearchHistory(keyword)
    }
})
观察者应用:购物车状态管理
// 购物车(被观察者)
class Cart {
    constructor() {
        this.items = []
        this.observers = [] // 观察者列表
    }
    
    // 添加观察者
    addObserver(observer) {
        this.observers.push(observer)
    }
    
    // 添加商品
    addItem(product) {
        this.items.push(product)
        this.notify() // 通知观察者
    }
    
    // 通知所有观察者
    notify() {
        this.observers.forEach(obs => obs.update(this.items))
    }
}
// 创建购物车实例
const cart = new Cart()
// 购物车图标(观察者)
cart.addObserver({
    update: (items) => {
        document.getElementById('cart-count').textContent = items.length
    }
})
// 结算面板(观察者)
cart.addObserver({
    update: (items) => {
        const total = items.reduce((sum, item) => sum + item.price, 0)
        document.getElementById('cart-total').textContent = `¥${total}`
    }
})
// 添加商品按钮
document.getElementById('add-to-cart').addEventListener('click', () => {
    cart.addItem({ name: 'JavaScript教程', price: 99 })
})
模式进阶:实现简单响应式系统
Proxy 是 ES6 引入的元编程特性,相比 Object.defineProperty 而言提供了更全面的拦截能力
// 创建响应式对象
function reactive(obj) {
    const observers = new Map() // 存储属性与观察者的映射
    
    return new Proxy(obj, {
        get(target, key) {
            return target[key]
        },
        set(target, key, value) {
            target[key] = value
            // 通知该属性的所有观察者
            if (observers.has(key)) {
                observers.get(key).forEach(cb => cb(value))
            }
            return true
        }
    })
}
// 添加属性观察
function watch(obj, key, callback) {
    if (!obj.observers) obj.observers = new Map()
    if (!obj.observers.has(key)) {
        obj.observers.set(key, [])
    }
    obj.observers.get(key).push(callback)
}
// 使用示例
const state = reactive({ count: 0, theme: 'light' })
// 监听count变化
watch(state, 'count', (value) => {
    console.log(`count更新为: ${value}`)
    document.getElementById('counter').textContent = value
})
// 监听theme变化
watch(state, 'theme', (value) => {
    console.log(`主题切换为: ${value}`)
    document.body.className = value
})
// 修改状态
state.count = 5 // 触发更新: count更新为: 5
state.theme = 'dark' // 触发更新: 主题切换为: dark
总结:两种模式的核心要点
| 概念 | 关键特点 | 代码示例 | 
|---|---|---|
| 发布订阅 | 完全解耦,通过事件中心通信 | eventBus.on('event', callback) | 
| 观察者 | 直接管理,状态变更主动通知 | subject.addObserver(observer) | 
| 适用场景 | 全局事件、模块间通信 | 组件通信、消息队列 | 
| 性能 | 事件中心可能成为性能瓶颈 | 直接通信效率更高 | 
| 复杂度 | 中等(需维护事件中心) | 简单(直接管理观察者) | 
理解这两种模式的差异是构建健壮JavaScript应用的关键。发布订阅模式像广播系统,适合需要完全解耦的场景;观察者模式像小组通知,适合紧密协作的场景。在实际项目中,Vue的EventBus使用发布订阅实现跨组件通信,而Vue的响应式系统则基于观察者模式实现数据绑定。