观察者模式在 Javascript 中的应用

3,944 阅读6分钟

*观察者模式(Observer Pattern)*在 Javascript 中应用非常普遍,本文会首先明确观察者模式在 Javascript 的基本实现方式,然后着眼与当前流行的工具库 —— RxJS —— 进一步研究观察者模式在其中的实现原理。


观察者模式的模式结构

观察者模式定义对象间的一种一对多依赖关系,使得当每一个被依赖对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

被依赖对象通常被称为主体(Subject),为了和 RxJS 中的概念一致,将其称为被观察对象(Observable),其相关依赖对象被称为观察者(Observer),即被观察对象会主动地向观察者“推送(push)”消息,而不是观察者向被观察对象“拉取(pull)”消息,实现的是一种 Push 模式的消息系统。

从被观察对象和观察者的类图可以明确地看到两者之间的消息传递关系:

观察者模式类图
观察者模式类图

被观察对象通过 subscribe 方法和 unsubscribe 方法添加和删除一个观察者,通过 broadcast 方法向观察者推送消息,接下来是一个最简单的观察者模式伪代码:

const clickHandler = (e) => { /* ... */ };
document.body.addEventListener('click', clickHandler);

document.body 在这里就是一个被观察对象, 全局对象是观察者,当 click 事件触发的时候,观察者会调用 clickHandler 方法。


实现一个 RxJS 风格的观察者模式

本文重点并不是如何使用 RxJS 工具,而是理解其基本的实现思想。所以接下来并不会详细说明 RxJS 的使用方法,而是自己动手实现一个具有类似功能的简易观察者模式,旨在学习如何自己在工作中运用观察模式。

首先介绍 RxJS 中最基本的使用方法。

第一步,创建一个 Observable 对象:

var observable = Rx.Observable.create(observer => {
  observer.next(1);
  observer.next(2);
  setTimeout(() => {
    observer.next(3);
    observer.complete();
  }, 1000);
});

第二步,在 observable 上订阅一个 observer

const observer = {
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
};
console.log('just before subscribe');
const subscription = observable.subscribe(observer);
console.log('just after subscribe');

接着就会看到输出结果:

just before subscribe
got value 1
got value 2
just after subscribe
got value 4
done

如果在调用了 subscribe 之后立即调用 unsubscribe ,则该 observer 会被取消订阅:

subscription.unsubscribe();

输出结果为:

just before subscribe
got value 1
got value 2
just after subscribe

即 1s 之后的执行动作被取消了。

通过上面的代码可以得知,RxJS 不仅使用了观察者模式还结合了迭代器模式的使用,本文重点关注其观察者模式的实现原理。

一共需要实现两个类,ObservableSubscriptionobservable 通过自身的 subscribe 方法订阅 observer 并且返回一个 subscription 对象,subscription 对象调用自身的 unsubscribe 方法可以当前 observer 的订阅,从输出结果上来看,即取消了 observernexterror 以及 complete 方法的执行。

注意 observer 并不需要实现,observer 是传入 Observable.prototype.subscribe 的参数。分析完毕之后,本文接下来会在 RxJS 的源码基础上构建一个简化版本实现。

首先是 Observable.js

module.exports = class Observable {
  constructor(_subscribe) {
    this._subscribe = _subscribe; // 传入的执行函数
  }
  static create(_subscribe) {
    return new Observable(_subscribe);
  }
  subscribe({ next, error, complete }) { // 解构传入的 observer
    const sink = new Subscription(next, error, complete);
    this._subscribe(sink);
    return sink;
  }
}

接下来是 Subscription.js

module.exports = class Subscription {
  constructor(next, error, complete) {
    this._isStopped = false;
    this._next = next;
    this._error = error;
    this._complete = complete;
  }
  next(value) {
    if (!this._isStopped) {
      this._next(value);
    }
  }
  error(value) {
    if (!this._isStopped) {
      this._isStopped = true;
      this._error(value);
      this.unsubscribe();
    }
  }
  complete(value) {
    if (!this._isStopped) {
      this._isStopped = true;
      this._complete(value);
      this.unsubscribe();
    }
  }
  unsubscribe() {
    this._isStopped = true;
  }
}

subscription 通过 _isStopped 标志显示是否调用了 unsubscribe 方法,并且将 observernexterrorcomplete 方法进行了进一步的包装,使得他们可以被取消订阅。

接下来使用类似调用 RxJS 的代码进行测试:

const Observable = require('./Observable');
const observable = Observable.create(observer => {
  observer.next(1);
  observer.next(2);
  setTimeout(() => {
    observer.next(3);
    observer.complete();
  }, 1000);
});
const observer = {
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
};
console.log('just before subscribe');
const subscription = observable.subscribe(observer);
console.log('just after subscribe');

输出结果和调用 RxJS 一致:

just before subscribe
got value 1
got value 2
just after subscribe
got value 4
done

如果在调用了 subscribe 之后立即调用 unsubscribe

subscription.unsubscribe();

输出结果为:

just before subscribe
got value 1
got value 2
just after subscribe

至此实现了一个建议版本的 RxJS 风格的观察者模式,RxJS 实在是微软的良心出品,建议每位前端工程师都能掌握。


观察者模式的实际用途

观察者模式的价值主要体现在以下两个方面:

  • 异步编程中广泛使用。

    观察者模式实现了异步对象的运行逻辑与异步回调逻辑的完全分离,以上面提到的 document.body.addEventListener 方法为例,接下来使用 RxJS 版本的观察者模式进行说明,首先是创建 Observable 对象:

    const observable = Rx.Observable.fromEvent(document.body, 'click');
    

    接着是订阅一个 Observer

    observable.subscribe({
      next: () => {/* .. */}
    })
    

    进行编写异步回调逻辑的时候,只需要关注 subscribe 函数的调用,完全不用关注异步运行逻辑的内部状态,程序的纯净性也得到了提高。

  • 程序便于扩展。

    由于观察者模式实现了对象运行逻辑与回调逻辑的松耦合,那么当出现新的 observer 时,就完全不用改变 observable 对象内部实现,而只需要再调用一次 Observable.prototype.subscribe 方法添加一个新的 observer,这样对于多人合作的项目来说,不仅便于分工也便于随时扩展。


观察者模式发布/订阅模式的比较

观察者模式发布/订阅模式的目标是一致的,都是主体主动地向通知观察者推送消息,然而观察者模式中主体和观察者还是存在一定的耦合性,两者必须确切的知道对方的存在才能进行消息的传递。

发布/订阅模式 更像是一个全局的观察者模式发布/订阅模式在主体与观察者之间引入消息调度中心,有点类似于 Node.js 中的 EventEmitter,主体和观察者之间完全透明,所有的消息传递过程都通过消息调度中心完成,也就是说具体的业务逻辑代码将会是在消息调度中心内,而主体和观察者之间实现了完全的松耦合,带来的问题也很直接:程序易读性显著降低。

观察者模式发布/订阅模式密不可分但又存在某种差异,并且各有利弊不能绝对的说孰优孰劣,使用何种模式需要根据实际应用场景进行选择。


参考资料