【问题标题】:rxjs switchMap need to return subscribed observablerxjs switchMap 需要返回订阅的 observable
【发布时间】:2026-01-20 20:00:01
【问题描述】:

这里是要求:

当点击开始按钮时,每 100 毫秒发出 x 次事件,每次发出对应一个 UI 更新。 x次emit完成后,会触发最后一次UI更新,看起来很简单吧?

这是我的代码:

const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel$ = interval(100)
    .pipe(
        take(x),
        share()
    )
var startLight$ = start$
    .pipe(
        switchMap(() => {
            intervel$
                .pipe(last())
                .subscribe(() => {
                    // Update UI
                })
            return intervel$
        }),
        share()
    )
startLight$
    .subscribe(function (e) {
        //Update UI
    })

显然,在switchMap 内订阅是反模式,所以我尝试重构我的代码:

const startInterval$ = start$
    .pipe(
        switchMapTo(intervel$),
    )
startInterval$.pipe(last())
    .subscribe(() => {
        //NEVER Receive value
    })

const startLight$ = startInterval$.pipe(share()) 

问题是intervel$流是在switchMap里面产生的,外面不能访问,只能访问产生intervel$的流,即start$永远不会完成!

是否有更聪明的方法来处理此类问题,或者这是 rxjs 的固有限制?

【问题讨论】:

    标签: rxjs observable


    【解决方案1】:

    你们很亲密。在 intervel$ 中使用 last() 仅将最后一个发送到下面的订阅。工作StackBlitz。以下是 StackBlitz 的详细信息:

    const start$ = fromEvent(document.getElementById('start'), 'click');
    const intervel$ = interval(100)
        .pipe(
            tap(() => console.log('update UI')), // Update UI here
            take(x),
            last()
        );
    
    const startInterval$ = start$
        .pipe( switchMapTo(intervel$));
    
    startInterval$
        .subscribe(() => {
            console.log('will run once');
        });
    

    更新

    如果你不想使用tap(),那么你可以简单地让start$ 完成,只取第一个发射,然后用take(1)first() 完成。 Here is a new StackBlitz 显示这个。

    const start$ = fromEvent(document.getElementById('start'), 'click')
        .pipe(
          first()
        );
    const intervel$ = interval(100)
        .pipe( 
          take(x) 
        );
    
    const startInterval$ = start$
        .pipe( 
          switchMapTo(intervel$)
        );
    
    startInterval$
        .subscribe(
          () => console.log('Update UI'),
          err => console.log('Error ', err),
          () => console.log('Run once at the end')
        );
    

    这种方法(或任何完成 Observable 的方法)的缺点是一旦完成就不会被重用。因此,例如,在新的 StackBlitz 中多次单击按钮将不起作用。使用哪种方法(第一种可以反复点击或完成的方法)取决于您需要的结果。

    另一种选择

    创建两个intervel$ observable,一个用于中间 UI 更新,一个用于最后一个。将它们合并在一起,只在订阅中更新 UI。 StackBlitz 这个选项

    代码:

    const start$ = fromEvent(document.getElementById('start'), 'click')
    const intervel1$ = interval(100)
        .pipe( 
          take(x)
        );
    const intervel2$ = interval(100)
        .pipe(
          take(x+1),
          last(),
          mapTo('Final')
        );
    
    const startInterval$ = start$
        .pipe( 
          switchMapTo(merge(intervel1$, intervel2$))
        );
    
    startInterval$
        .subscribe(
            val => console.log('Update UI: ', val)
        );
    

    一种更惯用的方式,与前一种逻辑相同(Guichi)

    import { switchMapTo, tap, take, last, share, mapTo } from 'rxjs/operators';
    import { fromEvent, interval, merge } from 'rxjs';
    
    const x = 5;
    
    const start$ = fromEvent(document.getElementById('start'), 'click');
    
    
    const intervel$ = interval(100);
    
    const intervel1$ = intervel$
      .pipe(
        take(x)
      );
    const intervel2$ = intervel1$
      .pipe(
        last(),
        mapTo('Final')
      );
    
    const startInterval$ = start$
      .pipe(
        switchMapTo(merge(intervel1$, intervel2$))
      );
    
    startInterval$
      .subscribe(
        val => console.log('Update UI: ', val)
      );
    

    反射

    原始问题的关键问题是“以不同的方式使用相同的 observable”,即在进度和决赛期间。所以merge 是针对此类问题的一个相当不错的逻辑模式

    【讨论】:

    • @Fan Cheung 谢谢你们两位,你们基本上提供了保存解决方案!他们绝对是有帮助的。但是有人认为“将重要的副作用从 do 转移到订阅”[egghead.io/lessons/…,这是一个不错的建议还是任何实现方式?
    • 另外,我想subject 可以以某种方式解决这个问题。但是subject 对我来说很陌生。只是一个模糊的想法,它可能会起作用。有可能充实吗?
    • 我很少在 subscribe() 中放任何东西,因为流假设是可重用、可组合的。大多数情况下,您在开发功能时需要大量改进和重构流的片段,如果您需要额外的逻辑,只需链接和扩展流并将其保存为另一个流。
    • @Guichi - 我在上面添加了一个更新,其中包含 Observable 完成的替代解决方案。
    • 是的,正如我提到的,这是完成 Observable 的缺点。好的,我的最后一个想法如下:创建两个 intervel - 一个用于中间 UI 更新,一个用于最后一个。将它们合并在一起并仅在订阅中更新 UI,无需点击:StackBlitz
    【解决方案2】:

    把你的更新逻辑放在switchMaptap()里面,tap会运行多次,subscribe()只会获取最后一次发射

    const startInterval$ = start$
        .pipe(
            switchMap(()=>intervel$.pipe(tap(()=>//update UI),last()),
        )
    startInterval$
        .subscribe(() => {
    //    will run one time
        })
    

    【讨论】:

      最近更新 更多