【问题标题】:Angular - RxJS : afterViewInit and Async pipeAngular - RxJS:afterViewInit 和异步管道
【发布时间】:2020-10-08 03:44:25
【问题描述】:

我尝试在使用 changeDetection: ChangeDetectionStrategy.OnPush, 的组件中执行以下操作

@ViewChild('searchInput') input: ElementRef;

ngAfterViewInit() {
    this.searchText$ = fromEvent<any>(this.input.nativeElement, 'keyup')
      .pipe(
        map(event => event.target.value),
        startWith(''),
        debounceTime(300),
        distinctUntilChanged()
      );
}

在模板中

<div *ngIf="searchText$ | async as searchText;">
  results for "<b>{{searchText}}</b>"
</div>

它不起作用,但是如果我删除OnPush,它会起作用。我不太清楚为什么,因为异步管道应该触发更改检测。

编辑

根据答案,我尝试用以下内容替换我所拥有的:

this.searchText$ = interval(1000);

没有任何@Inputasync 管道正在标记我的组件以供检查,它工作得很好。所以我不明白为什么我没有与fromEvent 相同的行为

【问题讨论】:

    标签: javascript angular typescript rxjs


    【解决方案1】:

    changeDetection: ChangeDetectionStrategy.OnPush 允许组件不会一直触发 changeDetection,而只是在更新 @Input() 引用时触发。因此,如果您在同一个组件中执行所有操作,则不会更新 @Input() 引用,因此不会更新视图。

    我建议你使用上面的模板代码创建你的哑组件,但通过@Input() 给它 searchText,并在你的智能组件中调用你的哑组件

    智能组件

    <my-dumb-component [searchText]="searchText$ | async"></my-dumb-component>
    

    哑组件

    @Input() searchText: SearchText
    

    模板

    <div *ngIf="searchText">
      results for "<b>{{searchText}}</b>"
    </div>
    

    【讨论】:

    • 我毫不怀疑它会起作用,但我不应该这样做。据我了解,组件内的异步管道应该可以完成工作。 blog.angular-university.io/onpush-change-detection-how-it-works
    • 这里的问题不是异步管道而是changeDetection,因为没有更新输入引用,这就是为什么changeDetection专门用于哑组件而不是智能组件,如果你不想改变您的结构,您必须删除 changeDetection onpush。在您的帖子中,使用了 smart-dumb 组件,并针对此结构进行了角推
    【解决方案2】:

    默认情况下,每当 Angular 启动更改检测时,它都会一个一个地检查所有组件并检查是否有任何更改,如果是,则更新其 DOM。当您将默认更改检测更改为 ChangeDetection.OnPush 时会发生什么?

    Angular 改变了它的行为,只有两种方法可以更新组件 DOM。

    • @Input 属性引用已更改

    • 手动调用markForCheck()

    如果您执行其中一项操作,它将相应地更新 DOM。在您的情况下,您不使用第一个选项,因此您必须使用第二个选项并在任何地方致电markForCheck()。但是有一种情况,每当你使用异步管道时,它都会为你调用这个方法。

    异步管道订阅 Observable 或 Promise 并返回 它发出的最新值。当发出一个新值时,异步 管道标记要检查更改的组件。当组件 被破坏,异步管道自动取消订阅以避免 潜在的内存泄漏。

    所以这里没有什么神奇之处,它在后台调用markForCheck()。但如果是这样,为什么您的解决方案不起作用?为了回答这个问题,让我们深入了解 AsyncPipe 本身。如果我们检查源代码 AsyncPipes 转换函数看起来像这样

    transform(obj: Observable<any>|Promise<any>|null|undefined): any {
        if (!this._obj) {
          if (obj) {
            this._subscribe(obj);
          }
          this._latestReturnedValue = this._latestValue;
          return this._latestValue;
        }
        ....// some extra code here not interesting
     }
    

    所以如果传递的值不是未定义的,它将订阅该 observable 并采取相应的行动(调用markForCheck(),每当值发出)

    现在是最关键的部分 Angular 第一次调用 transform 方法时,它是未定义的,因为你在 ngAfterViewInit() 回调中初始化了 searchText$(视图已经渲染,所以它也调用异步管道)。所以当你初始化searchText$字段时,这个组件的变化检测已经完成了,所以它不知道searchText$已经被定义了,随后它不再调用AsyncPipe,所以问题是它永远不会得到到 AsyncPipe 订阅这些更改,您要做的就是在初始化后仅调用一次 markForCheck(),Angular 再次在该组件上运行 changeDetection,更新 DOM 并调用 AsyncPipe,它将订阅该 observable

    ngAfterViewInit() {
        this.searchText$ =
         fromEvent<any>(this.input.nativeElement, "keyup").pipe(
          map((event) => event.target.value),
          startWith(""),
          debounceTime(300),
          distinctUntilChanged()
        );
        this.cf.markForCheck();
      }
    

    【讨论】:

    • 我很困惑,我真的不应该这样做,异步管道应该将组件标记为检查。没有任何输入,如果我在其上创建一个间隔(1000)和异步管道,我的模板将使用 OnPush 进行更新
    • 所以你的意思是使用间隔,而不是 fromevent?
    • 你确定它工作正常吗?因为它应该是一样的,我想我会测试它
    • this.searchText$ = interval(1000) .pipe(map(() => this.input.nativeElement.value)) .pipe(startWith(""), debounceTime(300), distinctUntilChanged ()); //那是代码对吗?
    • 我在做测试时只有 this.searchText$ = interval(1000)
    【解决方案3】:

    这是因为 Angular 在 ngAfterViewInitngAfterViewChecked 之前更新了 DOM 插值。我知道这听起来有点混乱。这是因为 Angular 的第一个变化检测周期。参考Max Koretskyi's article about change detection algorithm of Angular,在变更检测周期中,这些会依次发生:

    1. 如果第一次检查视图,则将 ViewState.firstCheck 设置为 true,如果之前已经检查过,则设置为 false
    2. 检查和更新子组件/指令的输入属性 实例
    3. 更新子视图更改检测状态(更改检测的一部分 战略实施)
    4. 为嵌入视图运行更改检测(重复中的步骤 列表)
    5. 如果绑定,则在子组件上调用 OnChanges 生命周期挂钩 改变了
    6. 在子组件上调用 OnInit 和 ngDoCheck(调用 OnInit 仅在第一次检查时)
    7. 更新子视图组件上的 ContentChildren 查询列表 实例
    8. 调用 AfterContentInit 和 AfterContentChecked 生命周期挂钩 子组件实例(AfterContentInit 仅在 第一次检查)
    9. 如果属性打开,则更新当前视图的 DOM 插值 当前视图组件实例已更改
    10. 为子视图运行更改检测(重复此步骤中的步骤) 列表)
    11. 更新当前视图组件上的 ViewChildren 查询列表 实例
    12. 在子节点上调用 AfterViewInit 和 AfterViewChecked 生命周期挂钩 组件实例(AfterViewInit 仅在第一次调用 检查)
    13. 禁用对当前视图的检查(更改检测的一部分 战略实施)

    如你所见,Angular 在 AfterContentInitAfterContentChecked 钩子被调用之后更新 DOM 插值(在第 9 步),所以如果你在 AfterContentInit 或 AfterContentChecked 生命周期钩子(或更早,如 OnInit 等)您的 DOM 将被更新,因为 Angular 在第 10 步更新 DOM,并且当您在 ngAfterViewInit() 中更改某些内容并且您正在使用 OnPush , Angular 不会更新 DOM,因为您在 ngAfterViewInit() 的第 12 步,并且在您更改某些内容之前 Angular 已经更新了 DOM!

    有一些解决方案可以避免这种情况,以便在 ngAfterViewInit 中订阅它。首先,你可以调用 markForCheck() 函数,所以你基本上在第一个周期中使用它说“嘿 Angular,你在第 9 步更新了 DOM,但我在第 12 步有一些东西要更改,所以请小心,有一个看看 ngAfterViewInit 我还有什么要改变的”。或者作为第二种解决方案,您可以再次手动触发更改检测(通过触发和事件处理程序或使用 ChangeDetectorRef 的 detecthanges() 函数),以便 Angular 再次重复所有这些步骤,当它再次到达第 9 步时,Angular 会更新您的DOM。

    我创建了一个Stackblitz example,你可以试试这些。您可以逐个取消注释放置在生命周期挂钩中的订阅行,以便您可以看到 Angular 在哪个生命周期挂钩之后更新 DOM。或者您可以尝试手动触发事件或触发更改检测周期,然后查看 Angular 在下一个周期更新 DOM。

    【讨论】:

      猜你喜欢
      • 2019-04-07
      • 2018-04-18
      • 1970-01-01
      • 2020-01-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-22
      • 1970-01-01
      相关资源
      最近更新 更多