关于 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
(观察者),所以 Subject
有 subscribe
方法(继承自 Observable
),也有 next
、complete
、error
方法(继承自 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
方法,其实也是 Observable
的 subscribe
,这个在上篇文章里已经提到过了,最终会调用 _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
,这个 _subscribe
在 Observable
和 Subject
中都有,相当于 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
,同时也重写了 error
、complete
、unsubscribe
,重写的目的都是为了管理 observers
队列,最终都还是调用 observers
中每个 item
对应的 error
、complete
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
上有三个重要的方法:next
、complete
、error
,都是用于操作数据流的方法,也就是说会对外隐藏这三个方法(调用不到),为什么要这么做呢?
文章开头提到, Subject
既是一个 Observable
(可观察对象)也是一个 Observer
(观察者),看着似乎能力更大了,但这同样也意味着风险更大了
本质上,你只是想创建一个多播的 Observable
,而你所期望的 Observable
,应该只对外提供 subscribe
和 unsubscribe
两个主力方法,外界只能读取到数据,内部数据的产生和流动对外界应当是隐藏的,如果外界能随意调用 next
、error
、complete
等能够操作数据流的方法,则很可能会打乱你预想的数据流动逻辑
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
是多播的,所以在任意位置调用 next
、error
、complete
方法,会对所有的 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
,但却无法调用 next
、error
、complete
了
但是有时候我又想让外界参与到对这三个方法的调用过程,因为我可能需要根据外界的状态来更改内部数据,那么可以让外界间接调用,先把这三个方法包装一层,也就是增加了一些自定义逻辑(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
,而继承于 Observable
的 Subject
被称为 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
而言,所有的订阅者都有各自不同的数据来源,所以 observable
与 observer
是一对一的关系
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
而言,所有的订阅者都有相同的数据来源,所以 observable
与 observer
是一对多的关系
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
处理的 observable
在 subscribe
之后不会有任何响应,只有在调用实例上的 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)
,source
是 connectable
方法传入的第一个 Observable
参数,这个参数一旦调用 subscribe
方法,那么就会立即执行回调方法,在我们的例子里,也就是会立即执行 subscribe => subscribe.next(Math.random())
,这里的 subscribe
是什么呢?巧了,就是被 SafeSubscriber
包装过的 subject
,也就是相当于是 subject.next(Math.random())
由于 subject
通过 result.subscribe
也 subscribe
了一些 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
这种应该就是为了保护单一数据流,在上一个数据流没有结束之前,不允许开启下一个
小结
从 Observable
到 Subject
是一个循序渐进的过程,因为 Observable
本身单播的局限性,所以出现了多播的 Subject
,同样的,Subject
也有其自身的局限性,所以在 Subject
的基础上,rxjs
还封装了一些 Subject
的变种