Rxjs源码解析(二)Subject

1,397 阅读8分钟

关于 Subject 的定义,官方源码的注释中已经解释得很清楚了

/**
 * A Subject is a special type of Observable that allows values to be
 * multicasted to many Observers. Subjects are like EventEmitters.
 *
 * Every Subject is an Observable and an Observer. You can subscribe to a
 * Subject, and you can call next to feed values as well as error and complete.
 */

Subject 是一个多播的 Observable,就像是 EventEmitters,每个 Subject 既是一个 Observable(可观察对象)也是一个 Observer(观察者),所以 Subjectsubscribe方法(继承自 Observable),也有 nextcompleteerror 方法(继承自 Subscription

new Subject 开始

const subject = new Subject<number>();

subject.subscribe(data => console.log('observerA: ', data));
subject.subscribe(data => console.log('observerB: ', data));
 
subject.next(1);
subject.next(2);

// observerA: 1
// observerB: 1
// observerA: 2
// observerB: 2

直接看 Subject 是个什么东西

// node_modules/rxjs/src/internal/Subject.ts

export class Subject<T> extends Observable<T> implements SubscriptionLike {
  constructor() {
    super();
  }
}

Subject 继承了 Observable,并且实现了 SubscriptionLike,印证了文章开头那段话,所以接下来调用的 subscribe方法,其实也是 Observablesubscribe,这个在上篇文章里已经提到过了,最终会调用 _trySubscribe

_trySubscribe 这个方法原本在 Observable 也有,但 Subject 重写了这个方法,不过也只是简单地包了一层,增加了一个异常处理,核心依旧是调用 Observable_trySubscribe

// node_modules/rxjs/src/internal/Subject.ts
protected _trySubscribe(subscriber: Subscriber<T>): TeardownLogic {
  this._throwIfClosed();
  return super._trySubscribe(subscriber);
}

protected _throwIfClosed() {
  if (this.closed) {
    throw new ObjectUnsubscribedError();
  }
}

Observable_trySubscribe 会调用 this._subscribe,这个 _subscribeObservableSubject 中都有,相当于 Subject 又覆写了这个方法,所以会调用 Subject_subscribe

// node_modules/rxjs/src/internal/Subject.ts
protected _subscribe(subscriber: Subscriber<T>): Subscription {
  this._throwIfClosed();
  this._checkFinalizedStatuses(subscriber);
  return this._innerSubscribe(subscriber);
}

protected _innerSubscribe(subscriber: Subscriber<any>) {
  const { hasError, isStopped, observers } = this;
  return hasError || isStopped
    ? EMPTY_SUBSCRIPTION
    : (observers.push(subscriber), new Subscription(() => arrRemove(observers, subscriber)));
}

_subscribe方法体内前两个方法调用都是异常处理不用多加关心,看最后一个方法 _innerSubscribe,在正常情况下会执行 (observers.push(subscriber), new Subscription(() => arrRemove(observers, subscriber))

Subject 是一个支持多播的 Observable,当有事件发生的时候,会把事件发送给所有的 observers(观察者),它是怎么知道有哪些 observers的呢?其实就是在 subscribe的时候,将所有的 observer 存到 observers 这个数组里

_innerSubscribe最终会返回 new Subscription(() => arrRemove(observers, subscriber)),也就是一个订阅,从上文可知,new Subscription传入的初始化参数 () => arrRemove(observers, subscriber),将会被存到 Subscription实例的 initialTeardown 属性上,当 subscription 执行 unsubscribe 方法的时候,也会执行 initialTeardown 方法,在这里,也就是将 subscriber 又从 observers 移除出去,达到了对 observer 灵活管理的目的

// node_modules/rxjs/src/internal/Subject.ts
next(value: T) {
  errorContext(() => {
    this._throwIfClosed();
    if (!this.isStopped) {
      const copy = this.observers.slice();
      for (const observer of copy) {
        observer.next(value);
      }
    }
  });
}

Subject 同样覆写了 next 方法,可以看到,确实是从 this.observers 中取值,然后遍历每个 item 执行 next 方法

Observable 是在 subscribe 的时候就立即通知 observer,而 Subject 则只是将传入的 observer 存下来,将 Subscriber暴露出去,等到外界主动调用 next 方法的时候才通知所有的 observer,这就是中间人设计模式

Subject 不仅重写了 next,同时也重写了 errorcompleteunsubscribe,重写的目的都是为了管理 observers 队列,最终都还是调用 observers 中每个 item 对应的 errorcomplete

error(err: any) {
  // ...
  const { observers } = this;
  while (observers.length) {
    observers.shift()!.error(err);
  }
  // ...
}
complete() {
  // ...
  const { observers } = this;
  while (observers.length) {
    observers.shift()!.complete();
  }
  // ...
}

unsubscribe() {
  this.isStopped = this.closed = true;
  this.observers = null!;
}

Subject 里还有一个重要的方法:asObservable,这个方法的名称比较语义化,不看逻辑就能猜到其应该是能够将 subject 当做 observable 来使用,但是 Subject 费劲巴拉地在 Observable 的基础上增加了很多看着不错的逻辑,实现了多播的 Observable,为什么又要倒回去?

/**
 * Creates a new Observable with this Subject as the source. You can do this
 * to create customize Observer-side logic of the Subject and conceal it from
 * code that uses the Observable.
 * @return {Observable} Observable that the Subject casts to
 */
asObservable(): Observable<T> {
  const observable: any = new Observable<T>();
  observable.source = this;
  return observable;
}

asObservable 方法的注释大概解释了一下,意思就是说将 subject 实例作为 Observable 的数据源,这样我们就可以创建出一个自定义的观察者逻辑,并且还可以隐藏一些代码细节

隐藏了什么代码细节?Subject 原本既是一个 Observable(可观察对象)也是一个 Observer(观察者),现在直接变成一个 Observable,那么就相当于是抛弃了 Observer的身份,Observer 上有三个重要的方法:nextcompleteerror,都是用于操作数据流的方法,也就是说会对外隐藏这三个方法(调用不到),为什么要这么做呢?

文章开头提到, Subject 既是一个 Observable(可观察对象)也是一个 Observer(观察者),看着似乎能力更大了,但这同样也意味着风险更大了

本质上,你只是想创建一个多播的 Observable,而你所期望的 Observable,应该只对外提供 subscribeunsubscribe 两个主力方法,外界只能读取到数据,内部数据的产生和流动对外界应当是隐藏的,如果外界能随意调用 nexterrorcomplete 等能够操作数据流的方法,则很可能会打乱你预想的数据流动逻辑

function intervalOut() {
  const source$ = new Subject<number>()
  let i = 0
  setInterval(() => {
    source$.next(i++)
  }, 1000)
  return source$
}

const instance = intervalOut()
instance.subscribe(data => console.log('订阅A:', data))
instance.subscribe(data => console.log('订阅B:', data))

setInterval(() => {
  instance.next(9999999)
}, 900)

对于上述例子,你只是想每隔 1000ms 就通知一次所有的订阅者,但由于 instance上提供了 next 方法,所以外界完全可以调用这个 next 来打乱数据流动的速率,并且由于 instance 是多播的,所以在任意位置调用 nexterrorcomplete方法,会对所有的 observer 产生影响

function intervalOut() {
  const source$ = new Subject<number>()
  let i = 0
  setInterval(() => {
    source$.next(i++)
  }, 1000)
  return source$.asObservable()
}

const instance = intervalOut()
instance.subscribe(data => console.log('订阅:', data))

setInterval(() => {
  instance.next(9999999) // Error: Property 'next' does not exist on type 'Observable<number>'.
}, 900)

修改一下 intervalOut,返回 source$.asObservable(),那么外界依旧可以 subscribe,但却无法调用 nexterrorcomplete

但是有时候我又想让外界参与到对这三个方法的调用过程,因为我可能需要根据外界的状态来更改内部数据,那么可以让外界间接调用,先把这三个方法包装一层,也就是增加了一些自定义逻辑(customize Observer-side logic),再对外暴露出这些包装方法,外界在调用之前,就会先执行这些自定义逻辑

function buildTeam() {
  const source$ = new Subject<string>()
  return {
    observable: source$.asObservable(),
    broadcast: (level: number) => {
      if (level > 90) {
        source$.next('欢迎大佬加入')
      } else if (level < 60) {
        source$.next('来了个菜鸡')
      } else {
        source$.next('你来啦')
      }
    }
  }
}
const instance = buildTeam()
instance.observable.subscribe(data => console.log(data))
instance.broadcast(30)
instance.broadcast(70)
instance.broadcast(100)
// 来了个菜鸡
// 你来啦
// 欢迎大佬加入

外界可以通过 broadcast间接调用到 next,但如何调用这个next,还是由 broadcast 内部控制了一下,并不是外界想怎么操作就能怎么操作的

Cold Observable & Hot Observable

rxjs 中,基础的 Observable被称为 Cold Observable,而继承于 ObservableSubject被称为 Hot Observable

const sourceA = new Observable<number>(subscribe => {
  subscribe.next(Math.random())
})
sourceA.subscribe(data => console.log('A:', data))
sourceA.subscribe(data => console.log('B:', data))
// A: 0
// B: 1

sourceA 订阅了两次,每次的数据都是不一致的,也就是说每次 subscribe,都会执行一遍回调函数里的逻辑,对于 cold observable而言,所有的订阅者都有各自不同的数据来源,所以 observableobserver 是一对一的关系

let index = 0
const sourceB = new Subject()
sourceB.subscribe(data => console.log('A:', data))
sourceB.subscribe(data => console.log('B:', data))
sourceB.next(index++)
// A: 0
// B: 0

sourceB 也订阅了两次,但两个订阅者接收到的数据是一致的,逻辑只执行了一遍,然后把结果给所有的订阅者都发送了一遍,对于 cold observable而言,所有的订阅者都有相同的数据来源,所以 observableobserver 是一对多的关系

ConnectableObservable

上篇文章说了Observable,但还没说完,因为有些部分涉及到本文的 Subject,所以放到这里看,Observable 有个变种叫 ConnectableObservable,不过这个方法目前已经 deprecated了,官方建议使用 connectable 替代,那么就直接看 connectable

// node_modules/rxjs/src/internal/observable/connectable.ts
// Creates an observable that multicasts once `connect()` is called on it.

connectable 同样是创建了一个 observable,纯粹的 observable,只要调用 subscribe 方法就会理解执行回调发送数据,而经过 connectable 处理的 observablesubscribe 之后不会有任何响应,只有在调用实例上的 connect() 方法之后,才会广播事件

这样就将 subscribe 与广播事件分离了,可以在任何时候 subscribe,但只有调用了 connect的时候才广播,增加了灵活性

const conn1 = connectable(new Observable<number>(subscribe => subscribe.next(Math.random())))
conn1.subscribe(data => console.log('A:', data))
conn1.subscribe(data => console.log('B:', data))
// 必须调用 connect
conn1.connect()
// A: 0.5575458558084838
// B: 0.5575458558084838

connectable 是一个方法,接收一个必选参数 source 和一个可选参数 config

// node_modules/rxjs/src/internal/observable/connectable.ts
export function connectable<T>(source: ObservableInput<T>, config: ConnectableConfig<T> = DEFAULT_CONFIG): Connectable<T> {
  // ...
  const { connector, resetOnDisconnect = true } = config;
  let subject = connector();

  const result: any = new Observable<T>((subscriber) => {
    return subject.subscribe(subscriber);
  });
  // ...
}

首先定义了一个 subject变量,这个变量是函数的第二个 config参数对象的一个属性方法 connector 生成的,默认值是 () => new Subject<unknown>(),也就是说 suject 的默认值就是一个 Subject 实例

export interface ConnectableConfig<T> {
  connector: () => SubjectLike<T>;
  resetOnDisconnect?: boolean;
}
const DEFAULT_CONFIG: ConnectableConfig<unknown> = {
  connector: () => new Subject<unknown>(),
  resetOnDisconnect: true,
};

然后又定义了一个 result 变量,是一个 Observable 的实例,这个实例的回调方法中,调用了 subject.subscribe

从前面的分析中,我们了解到,Observable 调用 subscribe 是会立即执行初始化的回调方法的,也就是只要调用 result.subscribe 方法。那么就会执行 subject.subscribe

// node_modules/rxjs/src/internal/observable/connectable.ts
export function connectable<T>(source: ObservableInput<T>, config: ConnectableConfig<T> = DEFAULT_CONFIG): Connectable<T> {
  // ...
  result.connect = () => {
    if (!connection || connection.closed) {
      connection = defer(() => source).subscribe(subject);
      if (resetOnDisconnect) {
        connection.add(() => (subject = connector()));
      }
    }
    return connection;
  };

  return result;
}

接着,在 result 这个 Observable 实例上塞了一个 connect方法,这个 connect 中重要的一句是 connection = defer(() => source).subscribe(subject);

defer 是一个 operator,这里可以认为是相当于是 source.subscribe(subject)sourceconnectable方法传入的第一个 Observable 参数,这个参数一旦调用 subscribe方法,那么就会立即执行回调方法,在我们的例子里,也就是会立即执行 subscribe => subscribe.next(Math.random()),这里的 subscribe 是什么呢?巧了,就是被 SafeSubscriber 包装过的 subject,也就是相当于是 subject.next(Math.random())

由于 subject 通过 result.subscribesubscribe 了一些 observer,那么在这个时候,就会把这些 observers 全拿出来依次执行一遍,在我们的例子里,也就是依次执行了 data => console.log('A:', data)data => console.log('B:', data) 这两个方法

所以,Subject 在这里再一次充当了中间人角色,完成了自己的多播任务

另外,在 defer(() => source).subscribe(subject); 语句的外面,实际上还是套了一个 if 条件语句的

if (!connection || connection.closed) {
  // ...
}

当第一次调用 .connect 执行到这个条件语句的时候,毫无疑问 !connection === true,所以可以进入逻辑,当在外界再一次调用 .connect 的时候,就得看 connection.closed的值了

connection.closed 在这里其实就是被 SafeSubscriber 包装过的 subject,那么只要调用 subject.complete 或者 subject.unsubscribe 或者 subject.error 方法,那么就会将 closed 置为 true

const conn1 = connectable(new Observable<number>(subscribe => {
  subscribe.next(1)
}))
conn1.subscribe(data => console.log('A:', data))
conn1.connect()
conn1.subscribe(data => console.log('B:', data))
conn1.connect()
// A: 1

这种情况下,因为 connection.closed === false,所以第二次调用 conn1.connect() 不会广播任何事件,但如果改成下面这样就没问题了

const conn1 = connectable(new Observable<number>(subscribe => {
  subscribe.next(1)
  // 这一句
  subscribe.complete()
}))
conn1.subscribe(data => console.log('A:', data))
conn1.connect()
conn1.subscribe(data => console.log('B:', data))
conn1.connect()
// A: 1
// B: 1

这种应该就是为了保护单一数据流,在上一个数据流没有结束之前,不允许开启下一个

小结

ObservableSubject 是一个循序渐进的过程,因为 Observable 本身单播的局限性,所以出现了多播的 Subject,同样的,Subject 也有其自身的局限性,所以在 Subject的基础上,rxjs还封装了一些 Subject的变种