JS `Observer Pattern`:实现事件订阅与发布,解耦模块

各位观众老爷们,早上好! 今天咱们来聊聊JavaScript中的“吃瓜群众”——观察者模式(Observer Pattern)。 别害怕,这名字听起来高大上,其实理解起来简单得很,就像咱们平时追剧、关注八卦一样。

一、什么是观察者模式?

想象一下,你特别喜欢某个明星,ta一发微博,你就立刻收到通知。这里,明星就是“被观察者”(Subject),你就是“观察者”(Observer),而微博平台就是连接你们的“中间人”。

观察者模式的核心思想就是:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动更新。

换句话说,就是“我变了,你们这些小弟都得跟着变!” (当然,这里的“小弟”是指依赖于被观察者的其他对象。)

二、观察者模式的组成部分

要实现一个观察者模式,至少需要以下几个角色:

  1. Subject (被观察者/目标): 维护一个观察者列表,提供添加、删除和通知观察者的方法。它的状态改变会触发通知。

  2. Observer (观察者): 定义一个更新接口,当接收到来自Subject的通知时,执行相应的更新操作。

  3. ConcreteSubject (具体被观察者): Subject的具体实现,负责维护自身状态,并在状态改变时通知所有观察者。

  4. ConcreteObserver (具体观察者): Observer的具体实现,接收Subject的通知,并执行相应的具体更新操作。

三、代码实现(简易版)

咱们先来一个最简单的版本,让你快速上手:

// Subject (被观察者)
class Subject {
  constructor() {
    this.observers = []; // 存储观察者
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

// Observer (观察者)
class Observer {
  constructor(name) {
    this.name = name;
  }

  update(data) {
    console.log(`${this.name} 收到通知:${data}`);
  }
}

// 使用
const subject = new Subject();

const observer1 = new Observer("小明");
const observer2 = new Observer("小红");

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify("老板发工资啦!"); // 小明和小红都会收到通知

subject.unsubscribe(observer1); // 小明不想再收到通知了

subject.notify("明天放假!"); // 只有小红会收到通知

这段代码模拟了一个简单的订阅/发布场景。 Subject 负责管理观察者列表,Observer 定义了接收通知的接口。 subscribe 方法用于添加观察者, unsubscribe 方法用于移除观察者, notify 方法用于通知所有观察者。

四、进阶版:带状态的观察者模式

上面的例子只是简单地传递了一条消息,但有时候我们需要传递被观察者的状态,让观察者根据状态进行不同的处理。

// Subject (被观察者)
class SubjectWithState {
  constructor() {
    this.observers = [];
    this.state = 0; // 初始状态
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  setState(newState) {
    this.state = newState;
    this.notify();
  }

  notify() {
    this.observers.forEach(observer => observer.update(this.state));
  }
}

// Observer (观察者)
class ObserverWithState {
  constructor(name) {
    this.name = name;
  }

  update(state) {
    console.log(`${this.name} 收到状态更新:当前状态是 ${state}`);
    if (state > 5) {
      console.log(`${this.name}:状态太高了,我要采取行动!`);
    }
  }
}

// 使用
const subjectWithState = new SubjectWithState();

const observer3 = new ObserverWithState("李雷");
const observer4 = new ObserverWithState("韩梅梅");

subjectWithState.subscribe(observer3);
subjectWithState.subscribe(observer4);

subjectWithState.setState(3); // 李雷和韩梅梅收到状态 3 的通知
subjectWithState.setState(7); // 李雷和韩梅梅收到状态 7 的通知,并且李雷和韩梅梅都会采取行动

在这个例子中, SubjectWithState 类维护了一个 state 属性,并在 setState 方法中更新状态并通知所有观察者。 ObserverWithState 类在 update 方法中接收状态,并根据状态执行不同的操作。

五、更灵活的实现:使用回调函数

有时候,我们不想为每个观察者都创建一个类,可以使用回调函数来实现更灵活的观察者模式。

// Subject (被观察者)
class SubjectWithCallback {
  constructor() {
    this.observers = [];
  }

  subscribe(callback) {
    this.observers.push(callback);
  }

  unsubscribe(callback) {
    this.observers = this.observers.filter(cb => cb !== callback);
  }

  notify(data) {
    this.observers.forEach(callback => callback(data));
  }
}

// 使用
const subjectWithCallback = new SubjectWithCallback();

const callback1 = (data) => {
  console.log(`回调函数1 收到通知:${data}`);
};

const callback2 = (data) => {
  console.log(`回调函数2 收到通知:${data}`);
};

subjectWithCallback.subscribe(callback1);
subjectWithCallback.subscribe(callback2);

subjectWithCallback.notify("今天加班!"); // 回调函数1和回调函数2都会收到通知

subjectWithCallback.unsubscribe(callback1); // 移除回调函数1

subjectWithCallback.notify("明天继续加班!"); // 只有回调函数2会收到通知

这种方式更加简洁,避免了创建大量类的麻烦。 直接将回调函数注册到 Subject 中,当 Subject 状态改变时,直接调用这些回调函数。

六、观察者模式的优点

  • 解耦性: Subject和Observer之间是松耦合的,Subject不知道Observer的具体实现,Observer也不知道Subject的内部状态。 它们之间只通过接口进行交互,降低了模块之间的依赖性。

  • 可扩展性: 可以很容易地添加新的Observer,而不需要修改Subject的代码。 符合开闭原则(对扩展开放,对修改关闭)。

  • 灵活性: 可以动态地添加和删除Observer,适应不同的需求。

七、观察者模式的缺点

  • 过度更新: 如果一个Subject有很多Observer,并且Subject的状态频繁改变,可能会导致Observer过度更新,影响性能。

  • 循环依赖: 如果Observer和Subject之间存在循环依赖,可能会导致无限循环。

  • 难以调试: 由于Observer和Subject之间是松耦合的,当出现问题时,可能难以追踪问题的原因。

八、观察者模式的应用场景

观察者模式在实际开发中应用非常广泛,以下是一些常见的应用场景:

  • 事件处理: DOM事件(如点击、鼠标移动等)就是典型的观察者模式的应用。 JavaScript中的addEventListener方法就是用于注册观察者,当事件发生时,浏览器会通知所有注册的观察者。

  • Model-View-Controller (MVC) 架构: 在MVC架构中,Model是被观察者,View是观察者。 当Model的数据发生改变时,会通知View进行更新。

  • 发布/订阅系统: 消息队列、事件总线等都是发布/订阅系统的典型应用。 发布者发布消息,订阅者订阅感兴趣的消息,当消息发布时,订阅者会收到通知。

  • 状态管理: Redux、Vuex等状态管理工具也使用了观察者模式。 当状态发生改变时,会通知所有订阅状态的组件进行更新。

九、观察者模式与其他模式的比较

  • 观察者模式 vs. 发布/订阅模式: 观察者模式和发布/订阅模式很相似,但也有一些区别。 观察者模式通常是同步的,Subject直接通知Observer。 而发布/订阅模式通常是异步的,发布者将消息发布到消息队列,订阅者从消息队列中获取消息。 发布/订阅模式更加灵活,可以实现更复杂的解耦。

  • 观察者模式 vs. 策略模式: 策略模式用于封装不同的算法,可以在运行时选择不同的算法。 而观察者模式用于解耦对象之间的依赖关系,当一个对象的状态改变时,通知其他对象。

  • 观察者模式 vs. 模板方法模式: 模板方法模式定义一个算法的骨架,将一些步骤延迟到子类实现。 而观察者模式用于实现对象之间的联动,当一个对象的状态改变时,通知其他对象。

十、总结

观察者模式是一种非常有用的设计模式,可以帮助我们解耦模块,提高代码的可扩展性和灵活性。 但是,也需要注意观察者模式的缺点,避免过度使用,导致性能问题或循环依赖。

特性 描述
核心概念 定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
组成部分 Subject (被观察者/目标), Observer (观察者), ConcreteSubject (具体被观察者), ConcreteObserver (具体观察者)
优点 解耦性,可扩展性,灵活性
缺点 过度更新,循环依赖,难以调试
应用场景 事件处理,Model-View-Controller (MVC) 架构,发布/订阅系统,状态管理
与其他模式比较 与发布/订阅模式的区别在于同步/异步,与策略模式的区别在于算法选择/依赖解耦,与模板方法模式的区别在于算法骨架/对象联动

希望今天的讲座能帮助大家更好地理解和应用观察者模式。 记住,代码的世界就像一个八卦论坛,掌握了观察者模式,你就能成为最灵通的“吃瓜群众”! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注