解耦的艺术:JavaScript发布订阅与观察者模式

72 阅读4分钟

本文通过代码实例和通俗解释,带你彻底理解JavaScript中两种核心设计模式的异同与应用场景

什么是发布订阅模式?

发布订阅模式(Pub/Sub) 是一种消息通信模式,它就像一家邮局:

  • 发布者是寄信人
  • 订阅者是收信人
  • 事件中心是邮局系统

核心特点

  1. 发布者和订阅者完全解耦,互不认识
  2. 所有通信通过事件中心传递
  3. 支持一对多的消息广播
// 邮局系统(事件中心)
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)是学生
  • 老师直接管理学生名单

核心特点

  1. 被观察者直接维护观察者列表
  2. 状态变更时直接通知所有观察者
  3. 被观察者知道观察者的存在
// 老师(被观察者)
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的响应式系统则基于观察者模式实现数据绑定。