我正在对从API检索到的某些数据使用缓存,出于逻辑原因,存储的数据仅在有限的时间内有效,因此我使用了类似的东西:
someApiData$ = this.getData()
.pipe(shareReplay(1, 3000))
对我来说似乎很明显,但显然shareReplay
运算符的创建者并不知道,如果不再缓存数据,则应重新获取它,或者至少我应该拥有另一个参数,它将为我提供此选项,例如:
someApiData$ = this.getData()
.pipe(shareReplay(1, 3000, shouldRefresh))
相反,下一个订阅者将获得的结果为null。 因此,我正在寻找解决此问题的理想方法。
答案 0 :(得分:1)
在此线程上的答案和网络上的其他一些方法中进行了一些跟踪后,这就是我的最终结果。它提供了以下能力:
Observable
我的缓存工具:
export class SharedReplayRefresh {
private sharedReplay$: Observable<T>;
private subscriptionTime: number;
sharedReplayTimerRefresh(
source: Observable<T>, bufferSize: number = 1,
windowTime: number = 3000000, scheduler?: SchedulerLike): Observable<T> {
const currentTime = new Date().getTime();
if (!this.sharedReplay$ ||
currentTime - this.subscriptionTime > windowTime) {
this.sharedReplay$ = source.pipe(shareReplay(
bufferSize, windowTime, scheduler));
this.subscriptionTime = currentTime;
}
return this.sharedReplay$;
}
}
我的数据服务:
export class DataService {
constructor(private httpClient: HttpClient) { }
private dataSource =
new SharedReplayRefresh<Data>();
private source = this.httpClient.get<Data>(url);
get data$(): Observable<Data> {
return this.dataSource .sharedReplayTimerRefresh(this.source, 1, 1500);
}
}
答案 1 :(得分:0)
根据documentation,window
运算符的shareReplay
参数不能这样工作:
该缓冲区中的项目可能会被丢弃而不会发送给后续观察者的时间(以毫秒为单位)
在您的代码示例中,这意味着3秒钟后新订阅者将一无所获。
我认为处理此问题的最佳方法是使用外部计数器进行处理:
private cache$: Observable<any>;
private lastTime: number;
public getCachedData() {
if (!this.cache$ || new Date().getTime() - this.lastTime > 3000) {
this.cache$ = this.getData().pipe(shareReplay(1));
this.lastTime = new Date().getTime();
}
return this.cache$;
}
每次新订户调用getCachedData()
时,此代码将“重新创建”可观察对象。
但是,较旧的订户不会获得新创建的新Observable的更新。为了使所有它们保持同步,您可能需要使用BehaviorSubject
来存储数据:
// Everybody subscribe to this Subject
private data$ = new BehaviorSubject(null);
public getCachedData() {
// TODO check time expiration here and call this.refreshData();
if(timeExpired) {
return this.refreshData().pipe(
mergeMap(data => {
return this.data$.asObservable();
})
);
} else {
return this.data$.asObservable();
}
}
private refreshData() {
return this.getData().pipe(
tap(data => {
this.data$.next(data);
})
);
}
上述解决方案只是一个想法,应该加以改进和测试。
答案 2 :(得分:0)
这是一种方法:
const URL = 'https://jsonplaceholder.typicode.com/todos/1';
const notifier = new Subject();
const pending = new BehaviorSubject(false);
const cacheEmpty = Symbol('cache empty')
const shared$ = notifier.pipe(
withLatestFrom(pending),
filter(([_, isPending]) => isPending === false),
switchMap(() => (
console.warn('[FETCHING DATA]'),
pending.next(true),
fetch(URL).then(r => r.json())
)),
tap(() => pending.next(false)),
shareReplay(1, 1000),
);
const src$ = shared$.pipe(
mergeWith(of(cacheEmpty).pipe(delay(0), takeUntil(shared$))),
tap(v => v === cacheEmpty && notifier.next()),
filter(v => v !== cacheEmpty)
)
src$.subscribe(v => console.log('[1]', v));
setTimeout(() => {
src$.subscribe(v => console.log('[2]', v));
}, 500);
setTimeout(() => {
src$.subscribe(v => console.log('[3]', v));
}, 1200);
mergeWith
是import { merge as mergeWith } from 'rxjs/operators'
(我认为从RxJs 7开始,它可以直接以mergeWith
的身份访问。)
我的理由是,我需要找到一种方法来确定正在使用的ReplaySubject
的缓存是否为空。众所周知,如果缓存不为空并且有新的订阅者到达,它将同步发送缓存的值。
所以
mergeWith(of(cacheEmpty).pipe(delay(0), takeUntil(shared$))),
本质上与
相同merge(
shared$,
of(cacheEmpty).pipe(delay(0), takeUntil(shared$)) // #2
)
如果缓存中有值,则将发出shared$
,而#2
将被取消订阅。
如果没有值,#2
将发出然后完成(它完成的事实不会影响外部的可观察对象)。
接下来,我们看到如果发出了cacheEmpty
,那么我们知道该刷新数据了。
tap(v => v === cacheEmpty && notifier.next()), // `notifier.next()` -> time to refresh
filter(v => v !== cacheEmpty)
现在,让我们看一下notifier
的工作方式
const shared$ = notifier.pipe(
// These 2 operators + `pending` make sure that if 2 subscribers register one after the other, thus synchronously
// the source won't be subscribed more than needed
withLatestFrom(pending),
filter(([_, isPending]) => isPending === false),
switchMap(() => (
console.warn('[FETCHING DATA]'),
pending.next(true), // If a new subscriber registers while the request is pending, the source won't be requested twice
fetch(URL).then(r => r.json())
)),
// The request has finished, we have the new data
tap(() => pending.next(false)),
shareReplay(1, 1000),
);
答案 3 :(得分:0)
我有一个类似的用例,最终使用了以下自定义运算符。
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
export const cacheValue = <T>(windowTime: (value: T) => number) => (
source: Observable<T>,
) => {
let cache: { value: T; expires: number } | undefined = undefined;
return new Observable<T>((observer) => {
if (cache && cache.expires > Date.now()) {
observer.next(cache.value);
observer.complete();
} else {
return source
.pipe(
tap(
(value) =>
(cache = { value, expires: Date.now() + windowTime(value) }),
),
)
.subscribe(observer);
}
});
};
如果缓存在100毫秒后到期,则将其称为cacheValue(() => 100)
,如果API返回的值具有expiresIn
属性,则将其称为cacheValue((value) => value.expiresIn)
。
答案 4 :(得分:0)
您可以更改流的启动方式,在这种情况下,使用 interval
创建一个立即发出的流,然后在满足间隔时使用它来触发数据加载。
当您第一次订阅流时,会触发间隔,加载数据,然后在三秒后再次订阅。
import { interval } from 'rxjs';
const interval$ = interval(3000); // emits straight away then every 3 seconds
当 interval$
发出时,使用 switchMap
切换出 Observable
并使用 shareReplay
以允许多播。
// previous import
import { switchMap, shareReplay } from 'rxjs/operators';
// previous code
const data$ = interval$.pipe(
switchMap(() => getData()),
shareReplay()
);
您还可以将 interval$ 包装在 merge
中,以便您可以像 Subject
一样根据 interval
创建手动刷新。
import { BehaviorSubject, merge, interval } from "rxjs";
import { shareReplay, switchMap } from "rxjs/operators";
const interval$ = interval(3000);
const reloadCacheSubject = new BehaviorSubject(null);
const data$ = merge(reloadCacheSubject, interval$).pipe(
switchMap(() => getData()),
shareReplay()
);
reloadCacheSubject.next(null); // causes a reload
StackBlitz 示例与 merge
和 refreshCache
Subject