我正在尝试使用RxJS缓存来避免不必要地重复某些HTTP调用。在尝试publishReplay
时,我得到了以下代码段(受this blog post的启发):
let counter = 1;
const updateRequest = Observable.defer(() => mockDataFetch())
.publishReplay(1, 1000)
.refCount();
function mockDataFetch() {
return Observable.of(counter++)
.delay(0); // <-- delay by 0 milliseconds
}
function mockHttpCache() {
return updateRequest
.take(1);
}
setTimeout(() => mockHttpCache().subscribe(val => console.log("Response 50:", val)), 50);
setTimeout(() => mockHttpCache().subscribe(val => console.log("Response 500:", val)), 500);
setTimeout(() => mockHttpCache().subscribe(val => console.log("Response 1500:", val)), 1500);
这按预期工作并产生输出:
'Response 50:', 1 'Response 500:', 1 'Response 1500:', 2
但是,当从内部可观察对象中删除.delay(0)
时,使其立即生效,在经过缓存持续时间之后,包装器不再产生任何结果。输出为:
'Response 50:', 1 'Response 500:', 1
即使没有缓存项了,看来也没有调用mockDataFetch
来收集新数据。这是预期的行为吗?如果是这样,其背后的原理是什么?
答案 0 :(得分:3)
这是将您的代码翻译成RxJs 6.5.5
以及一些其他小的修改:
let counter = 1;
const updateRequest = defer(() => mockDataFetch())
.pipe(
publishReplay(1, 1000),
refCount()
);
function mockDataFetch() {
console.log('RESUBSCRIBING');
return of(counter++)
.pipe(
// delay(0), // <-- delay by 0 milliseconds
);
}
function mockHttpCache() {
return updateRequest
.pipe(
take(1),
);
}
setTimeout(
() => mockHttpCache().subscribe(val => console.log("Response 50:", val), null, () => console.warn('complete[1]')
), 50);
setTimeout(
() => mockHttpCache().subscribe(val => console.log("Response 500:", val), null, () => console.warn('complete[2]')
), 500);
setTimeout(
() => mockHttpCache().subscribe(val => console.log("Response 1500:", val), null, () => console.warn('complete[3]')
), 1500);
delay(0)
我们首先来看看publishReplay
is implemented的方式:
const selector = typeof selectorOrScheduler === 'function' ? selectorOrScheduler : undefined;
const subject = new ReplaySubject<T>(bufferSize, windowTime, scheduler);
return (source: Observable<T>) => multicast(() => subject, selector!)(source) as ConnectableObservable<R>;
我们可以看到,由于multicast
,它返回了ConnectableObservable
:
const connectable: any = Object.create(source, connectableObservableDescriptor);
connectable.source = source;
connectable.subjectFactory = subjectFactory;
return <ConnectableObservable<R>> connectable;
这就是refCount
的样子:
// `connectable` - the `ConnectableObservable` from above
constructor(private connectable: ConnectableObservable<T>) { }
// `call` - called when the source is subscribed
// `source` - the `ConnectableObservable` from above
call(subscriber: Subscriber<T>, source: any): TeardownLogic {
const { connectable } = this;
(<any> connectable)._refCount++;
const refCounter = new RefCountSubscriber(subscriber, connectable);
const subscription = source.subscribe(refCounter);
if (!refCounter.closed) {
(<any> refCounter).connection = connectable.connect();
}
return subscription;
}
现在,让我们仔细看看ConnectableObservable
,尤其是subscribe
方法:
// Invoked as a result of `const subscription = source.subscribe(refCounter);` from `refCount`
_subscribe(subscriber: Subscriber<T>) {
return this.getSubject().subscribe(subscriber);
}
protected getSubject(): Subject<T> {
const subject = this._subject;
if (!subject || subject.isStopped) {
this._subject = this.subjectFactory();
}
return this._subject!;
}
subjectFactory
返回ReplaySubject
实例的位置。在const subscription = source.subscribe(refCounter);
上发生的基本上是,将RefCounterSubscriber
添加到ReplaySubject
的活动订户列表中。 RefCounterSubscriber
跟踪订户数量,当没有更多订户时,它将在注册新订户(使用相同的ReplaySubject
)实例时自动订阅源。
接下来,将调用(<any> refCounter).connection = connectable.connect();
。
connectable.connect()
执行以下操作:
connect(): Subscription {
let connection = this._connection;
if (!connection) {
this._isComplete = false;
connection = this._connection = new Subscription();
connection.add(this.source
.subscribe(new ConnectableSubscriber(this.getSubject(), this)));
if (connection.closed) {
this._connection = null;
connection = Subscription.EMPTY;
}
}
return connection;
}
到达这些行时:
connection.add(this.source
.subscribe(new ConnectableSubscriber(this.getSubject(), this)));
实际上将订阅源(例如mockDataFetch()
)。
现在,of(counter)
的实现大致如下:
// In this case, `arr = [counter]`
new Observable(subscriber => {
for (let i = 0; i < arr.length; i++) {
subscriber.next(arr[i]);
}
subscriber.complete();
});
这意味着将首先到达take(1)
,并且当它发生时,它将发出该值,然后发送一个complete
通知(最终通过调用Subscriber._complete()
):>
protected _complete(): void {
this.destination.complete();
this.unsubscribe();
}
因此,除了在链中进一步发送complete
通知之外,它还将取消订阅。它最终将达到RefCounterSubscriber
的取消订阅逻辑,但将无法按预期运行 ,因为一切都同步发生。在正常情况下,如果ReplaySubject
没有任何订阅者,则将取消订阅源。
但是由于订阅者订阅源时没有订阅者,所以行为会略有不同。 ReplaySubject
的订户列表将为空,但不会取消订阅,因为如上所述,它仍处于订阅过程
最后意味着subscriber.complete();
将被调用,这反过来将导致ReplaySubject
收到complete
通知。但是请记住,当源将被重新订阅时,将使用相同的ReplaySubject
。
下次下次再次订阅源时,将到达these lines:
const refCounter = new RefCountSubscriber(subscriber, connectable);
// Subscribing to a **completed** Subject
// If the Subject is completed, an EMPTY subscription will be reached
const subscription = source.subscribe(refCounter);
if (!refCounter.closed) { // Since `closed === true`, this block won't be reached
(<any> refCounter).connection = connectable.connect();
}
// Returning the EMPTY subscription
return subscription;
这就是程序的流程,没有delay(0)
setTimeout(
// Source emits and the value is cached by the subject for 1 second
// `take(1)` is reached
// Send the value, then a `complete` notif.
// But since sending a `complete` notif involves unsubscribing as well
// The current subscriber will be removed from the `ReplaySubject`'s subscribers list
// Then, the `ReplaySubject` will receive the `complete` notification and the subject becomes **completed**
() => mockHttpCache().subscribe(val => console.log("Response 50:", val), null, () => console.warn('complete[1]')
), 50);
setTimeout(
// Subscribing to a **completed** subject, but because it's a `ReplaySubject`
// We'd still be getting the cached values, along with a `complete` notification
() => mockHttpCache().subscribe(val => console.log("Response 500:", val), null, () => console.warn('complete[2]')
), 500);
setTimeout(
// Since `1`'s time expired at 1 second, the `ReplaySubject` will only send a complete notification
() => mockHttpCache().subscribe(val => console.log("Response 1500:", val), null, () => console.warn('complete[3]')
), 1500);
这将被记录:
RESUBSCRIBING
Response 50:
1
complete[1]
Response 500:
1
complete[2]
complete[3]
delay(0)
这取决于上一节中提到的一些细节。
delay(0)
将在每个AsyncScheduler
通知中在nexted
(默认)中安排一个操作。该动作的任务是在0 ms
通过之后发出该接收到的值。它与使用setTimeout
基本相同,这意味着它不会同步。
但是,当使用of()
时,complete
通知将被同步发送。 This is how delay
deals with it:
protected _complete() {
// `this.queue` is populated when a `nexted` value arrives
if (this.queue.length === 0) {
this.destination.complete();
}
// Unsubscribe from the previous items from the chain
// What's further will **not** be affected
this.unsubscribe();
}
complete
通知最终将在队列为空时发送。但是请记住,这都是异步,这意味着RefCountSubscriber
的行为会正常。
这就是程序的流程,delay(0)
:
setTimeout(
// Subscribing to the source, which emits a value and a complete notif, synchronously
// `delay` schedules an action that will do its job in 0ms(still asynchronously)
// The value is emitted by the `delay`'s scheduled action
// `take(1)` is reached
// The value will be passed along then a `complete` notif will be sent
// Then, the source will be unsubscribed
// Due to `refCount`, the complete notif that came from the source
// Won't reach the `ReplaySubject`. as it will already be unsubscribed from the source
() => mockHttpCache().subscribe(val => console.log("Response 50:", val), null, () => console.warn('complete[1]')
), 50);
setTimeout(
// Since only `500ms` have passed, this subscriber will receive the cached value (`1`)
// and a `complete` notification, due to `take(1)`
// But since `take(1)` operates synchronously, the `RefCountSubscriber` would be closed already, so the source won't be re-subscribed (//1)
() => mockHttpCache().subscribe(val => console.log("Response 500:", val), null, () => console.warn('complete[2]')
), 500);
setTimeout(
// `1500ms` passed, since `1000ms` the cache is empty
// So the `take(1)` operator will receive nothing, meaning that the source
// will be re-subscribed
() => mockHttpCache().subscribe(val => console.log("Response 1500:", val), null, () => console.warn('complete[3]')
), 1500);
输出:
RESUBSCRIBING
Response 50:
1
complete[1]
Response 500:
1
complete[2]
RESUBSCRIBING
Response 1500:
2
complete[3]
//1
为了查看RefCountSubscriber
已关闭,您可以在SB项目中打开开发工具,按CTRL + P
,键入{{ 1}},并在第78行上放置一个日志点(例如:refCount.ts
):
'refCounter.closed', refCounter.closed
,如果您注释掉最后一个if (!refCounter.closed) { /* ... */ }
,
您应该会看到类似这样的内容:
setTimeout(() => {}, 1500)