【问题标题】:RxJS: show loading if request is slowRxJS:如果请求很慢则显示加载
【发布时间】:2016-02-18 17:23:52
【问题描述】:

本想用 RxJS 优雅的解决这个问题,但是尝试了各种方法后,我不知道该怎么做......

我的需求很普遍:我打了一个休息电话,即。我有一个承诺。 如果响应很快,我只想使用结果。 如果来得慢,我想显示一个微调器,直到请求完成。 这是为了避免旋转器闪烁,然后是数据。

也许可以通过制作两个可观察对象来完成:一个带有承诺,另一个带有超时并将微调器显示为副作用。 我尝试了switch(),但没有取得多大成功,可能是因为另一个可观察对象没有产生值。

有人实现过类似的东西吗?

【问题讨论】:

  • 两个承诺应该有效,是的。你能告诉我们你试过的代码吗?
  • 感谢@Bergi 的提问。我终于发布了一个解决方案,并附有我为这个实验制作的 Plunker 的链接(之前的尝试太混乱了......)。当然,欢迎使用其他解决方案
  • 大多数编码人员并不担心 spinner-flash。延迟微调永远无法消除短暂闪光的可能性,只能及时将其移至下游。而且延迟得越多,您的用户在没有后台活动指示的情况下离开的时间就越长,这是微调器的目的。另外,请记住,您在(本地)开发中遇到的超短延迟通常会在实时环境中增加(更繁忙的服务器、互联网延迟......)

标签: javascript promise spinner rxjs


【解决方案1】:

基于@PhiLho 的answer,我编写了一个可管道操作符,它正是这样做的:

export function executeDelayed<T>(
    fn : () => void,
    delay : number,
    thisArg? : any
) : OperatorFunction<T, T> {
    return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
        let timerSub = timer(delay).subscribe(() => fn());
        return source.pipe(
            tap(
                () => {
                    timerSub.unsubscribe();
                    timerSub = timer(delay).subscribe(() => fn());
                },
                undefined,
                () => {
                    timerSub.unsubscribe();
                }
            )
        );
    }
}

基本上它返回一个函数,该函数获取Observable source
然后它使用给定的delay 启动一个timer
如果此计时器发出next-事件,则调用该函数。
但是,如果源发出 next,则 timer 将被取消并开始一个新的。
在源的complete中,timer终于被取消了。 然后可以像这样使用此运算符:

this.loadResults().pipe(
    executeDelayed(
        () => this.startLoading(),
        500
    )
).subscribe(results => this.showResult())

我自己并没有编写很多运算符,所以这种运算符实现可能不是最好的,但它确实有效。
欢迎任何关于如何优化它的建议:)

编辑: 正如@DauleDK 提到的,在这种情况下,错误不会停止计时器,fn 将在delay 之后调用。如果这不是你想要的,你需要在tap中添加一个onError-callback,它调用timerSub.unsubscribe()

export function executeDelayed<T>(
    fn : () => void,
    delay : number,
    thisArg? : any
) : OperatorFunction<T, T> {
    return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
        let timerSub = timer(delay).subscribe(() => fn());
        return source.pipe(
            tap(
                () => {
                    timerSub.unsubscribe();
                    timerSub = timer(delay).subscribe(() => fn());
                },
                () => timerSub.unsubscribe(),   // unsubscribe on error
                () => timerSub.unsubscribe()
            )
        );
    }
}

【讨论】:

  • 只是我,还是在发出错误时也调用了 fn?
  • @DauleDK fndelay 之后调用,除非timerSub.unsubscribe() 被调用。对于错误,情况并非如此,因为onError 回调设置为undefined。所以,如果发生错误,fn 仍然会在delay 之后被调用。如果这不是你想要的,你需要在onError中添加timerSub.unsubscribe()
  • 你是个传奇。您在哪里学习/阅读了有关如何创建自定义管道运算符的信息?
  • @DauleDK 实际上我只是看了一些运营商here。然后我用一个简单的运算符声明,它仅在给定函数返回 true 时执行 flatMap,否则返回默认值。这个运算符非常简单,实际上我只需要复制+粘贴一个现有的运算符并对其进行一些自定义。所以,它没有什么传奇色彩:)
【解决方案2】:

这是我用过的一个例子。我们在这里假设您也获得了想要发送到服务器的数据作为 Observable,称为query$。传入的查询将触发loadResults 函数,该函数应返回一个承诺并将结果放入results$ observable。

现在的诀窍是使用observable$.map(() =&gt; new Date()) 来获取最后一个发出值的时间戳。

然后我们可以比较最后一个查询的时间戳和来自服务器的最后一个响应。

由于您不仅希望显示加载动画,还希望在显示动画之前等待 750 毫秒,因此我们引入了延迟时间戳。更多解释请参见下面的 cmets。

最后我们得到了包含truefalseisLoading$ Observable。订阅它,以便在显示/隐藏加载动画时收到通知。

const query$ = ... // From user input.

const WAIT_BEFORE_SHOW_LOADING = 750;

const results$ = query$.flatMapLatest(loadResults);

const queryTimestamp$ = query$.map(() => new Date());
const resultsTimestamp$ = results$.map(() => new Date());
const queryDelayTimestamp$ = (
    // For every query coming in, we wait 750ms, then create a timestamp.
    query$
    .delay(WAIT_BEFORE_SHOW_LOADING)
    .map(() => new Date())
);

const isLoading$ = (
    queryTimestamp$.combineLatest(
        resultsTimestamp$,
        queryDelayTimestamp$,
        (queryTimestamp, resultsTimestamp, delayTimestamp) => {
            return (
                // If the latest query is more recent than the latest
                // results we got we can assume that
                // it's still loading.
                queryTimestamp > resultsTimestamp &&
                // But only show the isLoading animation when delay has passed
                // as well.
                delayTimestamp > resultsTimestamp
            );
        }
    )
    .startWith(false)
    .distinctUntilChanged()
);

【讨论】:

  • 你真的不需要比较日期。您可以简单地为每个查询/延迟/等增加一个计数器。换句话说,将new Date() 替换为++myCounter。您还需要将delayTimestamp &gt; queryTimestamp 添加到您的isLoading 计算中,以防止旧查询影响结果。
【解决方案3】:

好吧,我在通勤中想了想,找到了解决办法……

你可以在http://plnkr.co/edit/Z3nQ8q找到我的实验场

简而言之,解决方案是实际订阅可观察到的处理微调器(而不是尝试以某种方式组合它)。 如果 Rest 请求的结果出现在 observable 触发之前,我们只是取消微调器的可处置(订阅),所以它什么也不做。 否则,observable 将触发并显示其微调器。然后我们可以在收到响应后隐藏它。

代码:

function test(loadTime)
{
  var prom = promiseInTime(loadTime, { id: 'First'}); // Return data after a while
  var restO = Rx.Observable.fromPromise(prom);

  var load = Rx.Observable.timer(750);
  var loadD = load.subscribe(
    undefined,
    undefined,
    function onComplete() { show('Showing a loading spinner'); });

  restO.subscribe(
    function onNext(v) { show('Next - ' + JSON.stringify(v)); },
    function onError(e) { show('Error - ' + JSON.stringify(e)); loadD.dispose(); },
    function onComplete() { show('Done'); loadD.dispose(); }
  );
}

test(500);
test(1500);

不确定这是否是使用 RxJS 执行此操作的惯用方式,但它似乎有效...... 当然,欢迎使用其他解决方案。

【讨论】:

    【解决方案4】:

    这是我的解决方案:

    public static addDelayedFunction<T>(delayedFunction: Function, delay_ms: number): (mainObs: Observable<T>) => Observable<T> {
        const stopTimer$: Subject<void> = new Subject<void>();
        const stopTimer = (): void => {
            stopTimer$.next();
            stopTimer$.complete();
        };
        const catchErrorAndStopTimer = (obs: Observable<T>): Observable<T> => {
            return obs.pipe(catchError(err => {
                stopTimer();
                throw err;
            }));
        };
        const timerObs: Observable<any> = of({})
            .pipe(delay(delay_ms))
            .pipe(takeUntil(stopTimer$))
            .pipe(tap(() => delayedFunction()));
        return (mainObs: Observable<T>) => catchErrorAndStopTimer(
            of({})
                .pipe(tap(() => timerObs.subscribe()))
                .pipe(mergeMap(() => catchErrorAndStopTimer(mainObs.pipe(tap(stopTimer)))))
        );
    }
    

    【讨论】:

      【解决方案5】:

      就在获取数据之前,即。创建微调器,为创建微调器的函数设置超时。假设您愿意等待半秒钟,直到显示微调器......它会是这样的:

      spinnerTimeout = setTimeout(showSpinner, 500)
      fetch(url).then(data => {
        if (spinner) {
          clearTimeout(spinnerTimeout) //this is critical
          removeSpinnerElement()
        }
        doSomethingWith(data)
      });
      

      编辑:如果不明显,如果数据早于 500 毫秒(ish)到达,则 clearTimer 会停止 showSpinner 执行。

      【讨论】:

      • 感谢您的回答,但我想要一个 RxJS 的解决方案。好吧,也许您使用 clearTimeout 的回答将我推向了正确的方向......所以谢谢。
      猜你喜欢
      • 2013-03-24
      • 1970-01-01
      • 2019-04-12
      • 2018-06-28
      • 2019-09-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多