一篇文章搞懂js观察者模式

在这里插入图片描述

目录

  1. 什么是观察者模式
  2. 观察者模式的核心概念
  3. 实现观察者模式
  4. 观察者模式的应用场景
  5. 观察者模式 vs 发布-订阅模式
  6. Event Bus/Event Emitter
  7. 观察者模式的优缺点
  8. 实际应用中的注意事项
  9. 总结

什么是观察者模式

观察者模式是一种行为设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象。当这个目标对象的状态发生变化时,它会通知所有观察者对象,使它们能够自动更新自己。

这种模式在软件设计中被广泛使用,特别是在处理分布式事件处理系统时。它提供了一种对象间通信的方式,同时保持了较低的耦合度。

现实生活中的例子

  1. 报纸订阅: 报社(目标)维护一份订阅者(观察者)列表。当有新的报纸出版时,报社会通知所有订阅者。

  2. 股票市场: 股票(目标)价格变动时,所有关注该股票的投资者(观察者)都会收到通知。

  3. 社交媒体: 当你关注某人(目标)时,你(观察者)会收到他们的所有动态更新。

在JavaScript中,观察者模式常常用于处理异步操作和事件驱动编程。

观察者模式的核心概念

观察者模式主要包含两个核心角色:

  1. 目标(Subject): 也称为被观察者或发布者

    • 维护一系列观察者
    • 提供添加和删除观察者的机制
    • 当状态发生变化时,通知所有观察者
  2. 观察者(Observer): 对目标的状态感兴趣的对象

    • 提供一个更新接口,用于当目标状态变化时接收通知
    • 定义了对象之间的一种一对多的依赖关系

观察者模式的工作流程

  1. 目标对象维护一个观察者列表。
  2. 当目标对象的状态发生变化时,它会遍历观察者列表。
  3. 调用每个观察者提供的更新方法。
  4. 观察者接收到通知后执行相应的操作。

这个过程可以用以下序列图表示:

┌─────────┐          ┌─────────┐          ┌─────────┐
│ Subject │          │Observer1│          │Observer2│
└────┬────┘          └────┬────┘          └────┬────┘
     │    addObserver     │                    │
     │<───────────────────│                    │
     │    addObserver     │                    │
     │<───────────────────────────────────────>│
     │                    │                    │
     │ setState           │                    │
     │─────┐              │                    │
     │     │              │                    │
     │<────┘              │                    │
     │    notify          │                    │
     │─────────────────────────────────────────>│
     │    update          │                    │
     │────────────────────>│                   │
     │    update          │                    │
     │─────────────────────────────────────────>│

实现观察者模式

让我们用JavaScript来实现一个更详细的观察者模式:

// 目标类
class Subject {
  constructor() {
    this.observers = new Set();  // 使用Set来存储观察者,避免重复
    this.state = null;
  }

  // 添加观察者
  addObserver(observer) {
    this.observers.add(observer);
  }

  // 移除观察者
  removeObserver(observer) {
    this.observers.delete(observer);
  }

  // 通知所有观察者
  notify() {
    for (let observer of this.observers) {
      observer.update(this.state);
    }
  }

  // 设置状态并通知观察者
  setState(state) {
    this.state = state;
    this.notify();
  }
}

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

  update(state) {
    console.log(`${this.name} received state update: ${state}`);
  }
}

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

const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.setState('New State');
// 输出:
// Observer 1 received state update: New State
// Observer 2 received state update: New State

subject.removeObserver(observer1);
subject.setState('Another State');
// 输出:
// Observer 2 received state update: Another State

在这个实现中:

  1. Subject 类使用 Set 来存储观察者,这样可以自动避免重复添加。
  2. setState 方法不仅设置状态,还会自动通知所有观察者。
  3. 观察者的 update 方法接收状态作为参数,使其能够根据新状态执行相应的操作。

ES5实现

对于需要支持旧版浏览器的情况,这里是一个ES5的实现:

function Subject() {
  this.observers = [];
  this.state = null;
}

Subject.prototype.addObserver = function(observer) {
  if (this.observers.indexOf(observer) === -1) {
    this.observers.push(observer);
  }
};

Subject.prototype.removeObserver = function(observer) {
  var index = this.observers.indexOf(observer);
  if (index > -1) {
    this.observers.splice(index, 1);
  }
};

Subject.prototype.notify = function() {
  for (var i = 0; i < this.observers.length; i++) {
    this.observers[i].update(this.state);
  }
};

Subject.prototype.setState = function(state) {
  this.state = state;
  this.notify();
};

function Observer(name) {
  this.name = name;
}

Observer.prototype.update = function(state) {
  console.log(this.name + ' received state update: ' + state);
};

// 使用方式与ES6版本相同

观察者模式的应用场景

观察者模式在许多场景下都非常有用,以下是一些具体的应用例子:

  1. 事件处理系统:
    在前端开发中,DOM事件就是一个典型的观察者模式应用。

    // 添加事件监听器(观察者)
    document.addEventListener('click', function() {
      console.log('Document was clicked');
    });
    
    // 触发事件(目标通知观察者)
    document.dispatchEvent(new Event('click'));
    
  2. MVC架构:
    Model作为目标,View作为观察者,当Model变化时通知View更新。

    class Model {
      constructor() {
        this.data = null;
        this.observers = [];
      }
    
      setData(data) {
        this.data = data;
        this.notifyAll();
      }
    
      addObserver(observer) {
        this.observers.push(observer);
      }
    
      notifyAll() {
        this.observers.forEach(observer => observer.update(this.data));
      }
    }
    
    class View {
      update(data) {
        console.log('View updated with data:', data);
      }
    }
    
    const model = new Model();
    const view = new View();
    
    model.addObserver(view);
    model.setData('New Data');
    
  3. 响应式编程:
    如RxJS库,就是基于观察者模式的。它提供了强大的工具来处理异步数据流。

    import { fromEvent } from 'rxjs';
    
    const clicks = fromEvent(document, 'click');
    const subscription = clicks.subscribe(x => console.log('Clicked!'));
    
    // 稍后取消订阅
    subscription.unsubscribe();
    
  4. 状态管理:
    在这里插入图片描述

    Redux等状态管理库的实现也用到了观察者模式的思想。当状态变化时,所有订阅了该状态的组件都会得到通知并更新。

    import { createStore } from 'redux';
    
    // Reducer
    function counter(state = 0, action) {
      switch (action.type) {
        case 'INCREMENT':
          return state + 1;
        case 'DECREMENT':
          return state - 1;
        default:
          return state;
      }
    }
    
    // Store
    let store = createStore(counter);
    
    // 订阅状态变化
    store.subscribe(() => console.log(store.getState()));
    
    // 改变状态
    store.dispatch({ type: 'INCREMENT' }); // 1
    store.dispatch({ type: 'INCREMENT' }); // 2
    store.dispatch({ type: 'DECREMENT' }); // 1
    
  5. 数据绑定:
    在这里插入图片描述

    在现代前端框架中,如VueReact,数据绑定的实现也用到了观察者模式的思想。

    // Vue示例
    new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      }
    });
    
    // 当message改变时,所有使用message的地方都会自动更新
    

这些例子展示了观察者模式在前端开发中的广泛应用。它不仅用于处理用户交互,还在数据流管理、状态同步等多个方面发挥着重要作用。

观察者模式 vs 发布-订阅模式

虽然观察者模式和发布-订阅模式经常被混用,但它们是有区别的:

  1. 耦合度:

    • 观察者模式中,目标直接知道观察者,耦合度较高。
    • 发布-订阅模式通过一个中间件(事件通道)来管理订阅关系,发布者和订阅者互不了解,完全解耦。
  2. 通信方式:

    • 观察者模式是同步的,当目标发生变化时,会立即调用观察者的方法。
    • 发布-订阅模式可以是异步的,通过事件通道,可以实现延迟传递。
  3. 应用场景:

    • 观察者模式适用于目标和观察者之间有稳定的逻辑关系的场景。
    • 发布-订阅模式更适合于功能模块之间完全独立的场景。
  4. 通知方式:

    • 观察者模式中,目标主动将自身变化通知给所有观察者。
    • 发布-订阅模式中,发布者不知道谁会接收通知,它只负责发布事件。
  5. 代码结构:

    • 观察者模式通常定义在单个对象上。
    • 发布-订阅模式经常使用全局的事件总线。

图示对比

观察者模式:

┌─────────┐          ┌─────────┐
│ Subject │◄─────────│Observer │
└─────────┘          └─────────┘
     │                    ▲
     │      notify        │
     └────────────────────┘

发布-订阅模式:

┌─────────┐    publish    ┌─────────────┐   subscribe   ┌─────────┐
│Publisher│───────────────►│Event Channel│◄──────────────│Subscriber│
└─────────┘               └─────────────┘               └─────────┘
                                │
                                │ notify
                                ▼
                          ┌─────────┐
                          │Subscriber│
                          └─────────┘

代码对比

观察者模式:

class Subject {
  constructor() {
    this.observers = [];
  }

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

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

class Observer {
  update(data) {
    console.log('Received:', data);
  }
}

const subject = new Subject();
const observer = new Observer();
subject.addObserver(observer);
subject.notify('Hello World');

发布-订阅模式:

class EventBus {
  constructor() {
    this.events = {};
  }

  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }

  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
}

const eventBus = new EventBus();
eventBus.subscribe('userLoggedIn', data => console.log('User logge[前文内容保持不变,接续上文]

d in:', data));
eventBus.publish('userLoggedIn', {id: 1, name: 'John'});

这个对比清楚地展示了两种模式在实现和使用上的差异。

Event Bus/Event Emitter

Event Bus或Event Emitter是发布-订阅模式的一种实现,它在前端开发中非常常见。它提供了一个集中的地方来管理应用程序中的事件。

基本实现

以下是一个简单但功能完整的Event Emitter实现:

class EventEmitter {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
    return () => this.off(eventName, callback); // 返回取消订阅的函数
  }

  // 发布事件
  emit(eventName, ...args) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      callbacks.forEach(callback => callback(...args));
    }
  }

  // 取消订阅
  off(eventName, callback) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      this.events[eventName] = callbacks.filter(cb => cb !== callback);
    }
  }

  // 只订阅一次
  once(eventName, callback) {
    const onceWrapper = (...args) => {
      callback(...args);
      this.off(eventName, onceWrapper);
    };
    return this.on(eventName, onceWrapper);
  }
}

使用示例

const eventBus = new EventEmitter();

// 订阅事件
const unsubscribe = eventBus.on('userAction', data => {
  console.log('User action:', data);
});

// 发布事件
eventBus.emit('userAction', {type: 'click', target: 'button'});

// 取消订阅
unsubscribe();

// 只订阅一次
eventBus.once('oneTimeEvent', () => {
  console.log('This will only be called once');
});

eventBus.emit('oneTimeEvent');
eventBus.emit('oneTimeEvent'); // 不会触发回调

在Vue中使用Event Bus

Vue 2.x版本中,可以使用一个空的Vue实例作为中央事件总线:

// 创建事件总线
export const EventBus = new Vue();

// 在组件A中发布事件
EventBus.$emit('itemAdded', { name: 'Item 1' });

// 在组件B中订阅事件
EventBus.$on('itemAdded', item => {
  console.log('Added item:', item);
});

注意: 在Vue 3中,推荐使用provide/inject API或Vuex来替代全局Event Bus。

React中使用Event Emitter

在这里插入图片描述

React没有内置的Event Bus,但我们可以使用上面实现的EventEmitter类:

import React, { useEffect } from 'react';
import { EventEmitter } from './eventEmitter';

const eventBus = new EventEmitter();

function ComponentA() {
  const handleClick = () => {
    eventBus.emit('buttonClicked', 'Hello from ComponentA');
  };

  return <button onClick={handleClick}>Click me</button>;
}

function ComponentB() {
  useEffect(() => {
    const unsubscribe = eventBus.on('buttonClicked', (message) => {
      console.log(message);
    });

    return unsubscribe; // 清理函数
  }, []);

  return <div>ComponentB</div>;
}

使用Event Bus可以使组件间的通信更加灵活,特别是对于深层嵌套的组件。但也要注意,过度使用可能会使应用的数据流变得难以跟踪和维护。

观察者模式的优缺点

优点

  1. 松耦合: 目标和观察者之间的关系是松散的,它们可以独立变化而不会相互影响。

  2. 支持广播通信: 一个目标可以通知多个观察者,无需知道具体有多少个观察者。

  3. 易于扩展: 可以轻松添加新的观察者,而无需修改现有代码。

  4. 符合开闭原则: 可以在不修改现有代码的情况下,通过添加新的观察者来扩展系统的功能。

缺点

  1. 性能问题: 如果观察者过多,通知所有观察者可能会导致性能下降。

  2. 可能导致循环依赖: 如果观察者和目标之间存在循环依赖,可能会导致系统陷入死循环。

  3. 通知次序不可控: 不能保证观察者收到通知的顺序。

  4. 意外的更新: 如果没有正确管理,可能会导致意外或不必要的更新。

实际应用中的注意事项

  1. 内存泄漏: 确保在不再需要观察者时将其移除,特别是在使用闭包时。

    class Subject {
      // ...其他代码...
      
      removeObserver(observer) {
        const index = this.observers.indexOf(observer);
        if (index > -1) {
          this.observers.splice(index, 1);
        }
      }
    }
    
    // 使用
    const observer = new Observer();
    subject.addObserver(observer);
    
    // 当不再需要时
    subject.removeObserver(observer);
    
  2. 状态一致性: 在通知观察者之前,确保目标的状态已经完全更新。

    class Subject {
      setState(newState) {
        // 先完全更新状态
        this.state = {...this.state, ...newState};
        // 然后通知观察者
        this.notify();
      }
    }
    
  3. 异常处理: 在通知观察者时,要处理可能出现的异常,以防止一个观察者的错误影响其他观察者。

    notify() {
      this.observers.forEach(observer => {
        try {
          observer.update(this.state);
        } catch (error) {
          console.error('Error notifying observer:', error);
        }
      });
    }
    
  4. 避免过度使用: 观察者模式可能导致代码难以理解和维护,特别是在大型系统中。适度使用,并考虑其他替代方案。

  5. 优化性能: 对于大量观察者的情况,考虑使用批量通知或节流技术。

    import { debounce } from 'lodash';
    
    class Subject {
      constructor() {
        this.observers = [];
        this.notifyObservers = debounce(this.notifyObservers.bind(this), 100);
      }
    
      notifyObservers() {
        this.observers.forEach(observer => observer.update(this.state));
      }
    
      setState(newState) {
        this.state = {...this.state, ...newState};
        this.notifyObservers();
      }
    }
    
  6. 测试: 在单元测试中,模拟观察者可以帮助确保目标正确地通知了所有观察者。

    describe('Subject', () => {
      it('should notify all observers when state changes', () => {
        const subject = new Subject();
        const observer1 = { update: jest.fn() };
        const observer2 = { update: jest.fn() };
    
        subject.addObserver(observer1);
        subject.addObserver(observer2);
    
        subject.setState({ foo: 'bar' });
    
        expect(observer1.update).toHaveBeenCalledWith({ foo: 'bar' });
        expect(observer2.update).toHaveBeenCalledWith({ foo: 'bar' });
      });
    });
    

总结

观察者模式是一种强大而灵活的设计模式,在JavaScript和前端开发中有广泛的应用。它允许对象之间保持一种松散的耦合,同时提供了一种有效的对象间通信机制。

通过本文,我们深入了解了观察者模式的核心概念、实现方式、应用场景,以及它与发布-订阅模式的区别。我们还探讨了Event Bus/Event Emitter的实现和使用,这是发布-订阅模式的一种常见变体。

在实际应用中,观察者模式可以大大提高代码的可维护性和可扩展性。然而,它也有其局限性,比如可能导致的性能问题和复杂性。因此,在使用观察者模式时,需要权衡其利弊,并根据具体情况选择是否使用。

最后,理解并掌握观察者模式,对于提高代码质量和系统设计能力都有很大帮助。它不仅是一种编程技巧,更是一种解决问题的思维方式。在JavaScript的异步编程、事件驱动编程中,观察者模式的思想无处不在。掌握这种模式,将帮助你更好地理解和使用各种现代前端框架和库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

貂蝉的腿毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值