【问题标题】:Timing problem between OnInit and async-pipeOnInit 和 async-pipe 之间的时序问题
【发布时间】:2020-07-27 22:49:39
【问题描述】:

async-pipe 与 Observable 的组合存在问题,它在 OnInit 中获得了第一个值。必须是关于OnInit 发生的时间点以及模板被渲染并因此Observable 被订阅的时间点的时间问题。

考虑这个组件:

export class AppComponent implements OnInit {

    subjectA$: Subject<{name:string}>;
    subjectB$: Subject<{name:string}>;

    constructor(
        protected http: HttpClient
    ) {
    }

    ngOnInit() {
        this.subjectA$ = new Subject<{name: string}>();
        this.subjectA$.next({name: "A"});

        this.subjectB$ = new Subject<{name: string}>();
        setTimeout(() => {
          this.subjectB$.next({name: "B"});
        }, 0);
    }

}

和模板:

<p *ngIf='subjectA$ | async as subjectA; else: nosubjectA'>
  subjectA: {{subjectA.name}}
</p>

<ng-template #nosubjectA>
  <p>no subjectA</p>
</ng-template>

<p *ngIf='subjectB$ | async as subjectB; else: nosubjectB'>
  subjectB: {{subjectB.name}}
</p>

<ng-template #nosubjectB>
  <p>no subjectB</p>
</ng-template>

这会导致

no subjectA

subjectB: B 

这意味着:即使subjectA$onInit 中获得了值,视图也不会更新。如果我在setTimeout 中创建第一个值,正如您在subjectB$ 中看到的那样,它可以工作并且我看到了值。虽然这是一个解决方案,但我想知道为什么会发生这种情况,是否有更好的解决方案?

我已经找到的一个解决方案是使用 BehaviorSubject 代替提供第一个值作为初始值:


        this.subjectC$ = new BehaviorSubject<{name: string}>({name: "C"});

使用类似的主题C模板导致subjectC: C

Try all on StackBlitz.

我真正可观察到的根本不是Subject,而是combineLatest-调用不同东西的结果,其中只有一个是(不幸的是,因为它使用来自@Input()-annotation 的值) 一个Subject,并在OnInit 中使用next 手动推送,如示例中所示。其余的来自http 等人。很可能我可以将组合结果包装在 BehaviourSubject 中,但它对我来说看起来既丑陋又危险,所以它比 setTimeout 方法更糟糕。但我敢打赌,有人可以帮助我并找到一个真正有用的解决方案。 此外,我更愿意避免使用BehaviorSubject,以防止开发人员被诱惑使用getValue

See on Stackblitz

【问题讨论】:

  • 最近遇到了类似的问题(特别是将@Input 转换为可观察对象),我能够通过使用shareReplay(1)(或使用缓冲区为1 的ReplaySubject)来解决可观察对象- 在ngOnInit 中执行 .next 的问题似乎是它在模板实际订阅 observables 之前首先运行。将事物移动到 ngAfterViewInit(或 ngAfterContentInit)并不能真正解决问题,因为更改检测会抱怨。所以剩下的就是重放最后一个值,将其存储在BehaviorSubject 中,或者也使用setTimeout
  • 好吧,这意味着我不应该太乐观地尝试除onInit之外的其他钩子......互联网上怎么会有使用async而不是手动订阅的建议,但不要没有合适的解决方案?

标签: angular rxjs async-pipe


【解决方案1】:

快速解决方法是将ReplaySubject 与缓冲区1 一起使用,而不是BehaviorSubject。您不必提供默认值,它既没有getValue() 函数也没有value getter。然而,它会缓冲(或保存)最后一个发出的值,并在新订阅时立即发出。

试试下面的

ngOnInit() {
  this.subjectA$ = new ReplaySubject<{name: string}>(1);
  this.subjectA$.next({name: "A"});

  this.subjectB$ = new ReplaySubject<{name: string}>(1);
  this.subjectB$.next({name: "B"});

  this.subjectC$ = combineLatest([this.subjectA$, this.subjectB$]).pipe(
    map((things: [{name:string}, {name:string}]): {name:string} => {
      return {name: things.map(thing => thing.name).join('|')}
    })
  );
}

我修改了你的Stackblitz

【讨论】:

  • 谢谢,毫无疑问比BehaviorSubject 更好的方法。但仍然感觉更像是一种解决方法而不是解决方案。根本问题显然来自ngInit 的顺序和async 的订阅,以及 Angular 在异步处理时处理下一次调用的方式(但仍然是因为setTimeout0 调用)。我想我很了解这些事情,可以想出一个直接接近 than 的解决方案(例如使用另一个钩子或其他方法)。
  • @Paflow:如果您想了解使用0 调用setTimeout 的原因,您可以查看here
【解决方案2】:

在我发表评论后,我真的忍不住想一定有更好的方法——终于想到了一些可行的方法!

我刚刚修改了你的堆栈闪电战。

private valueA = "A";
private valueB = "B";

subjectA$ = of({ name: this.valueA });
subjectB$ = of({ name: this.valueB });
subjectC$ = combineLatest([this.subjectA$, this.subjectB$])
          .pipe(
            map((things: [{name:string}, {name:string}]): {name:string} => {return {name: things.map(x => x.name).join('|')}})
          );

这样,我们甚至可以丢弃 ngOnInit 钩子,一切正常!

【讨论】:

  • 乍一看,这似乎是一个完美的解决方案,干净、简单,感觉不像是一种解决方法。对于许多类似的情况,可能就是这样。但正如我所写,我需要来自@Input()-annotation 的值来计算其中一个组合 Observables 的(第一个)内容 - 它在构建时不存在,但在 onInit 中。我想,解决方案是在构造时初始化 Observable,并在 onInit 中给它们它们的第一个值 - 但不,你仍然需要 setTimeout-hack。见:stackblitz.com/edit/nginit-async-problem-3
  • 抱歉……但我真的觉得这次我明白了。从现在开始,of 创建了一个热可观察对象,并且由于输入的值最初确实不可用,因此该值将不可用。所以我认为我们真正需要的是一个冷观测值。我更新了您的 stackblitz 以真正展示您的需求 - 使用单独的组件和 2 @Inputs a 和 b。 stackblitz.com/edit/nginit-async-problem-3-y6qxuo
  • 我很惊讶这行得通,当@Input 实际被填充时,但我认为这是一个合适的解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-06
  • 2021-10-18
  • 1970-01-01
  • 2012-03-25
  • 2020-09-16
相关资源
最近更新 更多