我有一个由2个组件组成的非常简单的测试应用

  • AppComponent
  • ListComponent

  • 这两个组件具有相同的行为:在加载时,它们使用异步管道显示来自Observable的项目列表。 AppComponent 另外还有一个按钮和一个RouterOutlet,单击该按钮后,它将在其中加载 ListComponent

    编译此代码以进行开发时,即使用ng build,一切都会按预期进行。当为产品编译相同的代码(即ng build --prod)时,行为就不同了。在第二种情况下,如果单击按钮转到 ListComponent ,则在页面加载时不再发出 ListComponent的可观察项

    代码如下(我也有a Stackblitz example,虽然没有发生问题)

    ListComponent
    @Component({
      selector: 'app-list',
      template: `
        <input #searchField type="text">
        <div *ngFor="let item of itemsToShow$ | async">{{item}}</div>
      `,
    })
    export class ListComponent implements OnInit, AfterViewInit  {
      @ViewChild('searchField', {static: true}) searchField: ElementRef;
      search$: Observable<string>;
      itemsToShow$: Observable<string[]>;
    
      ngOnInit() {}
    
      ngAfterViewInit() {
        this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
          map(() => this.searchField.nativeElement.value)
        );
    
        this.itemsToShow$ = this.itemsToShow();
      }
    
      itemsToShow() {
        let itemAsLocalVar: string[];
        return of(ITEMS).pipe(
          delay(10),
          tap(items => itemAsLocalVar = items),
          switchMap(() => combineLatest([this.search$])),
          map(([searchFilter]) => itemAsLocalVar.filter(i => i.includes(searchFilter))),
        );
      }
    }
    

    AppComponent
    @Component({
      selector: 'app-root',
      template: `
        <input #searchField type="text">
        <div *ngFor="let item of itemsToShow$ | async">{{item}}</div>
        <router-outlet></router-outlet>
        <button (click)="goToList()">List</button>
      `
    })
    export class AppComponent implements OnInit, AfterViewInit  {
      @ViewChild('searchField', {static: true}) searchField: ElementRef;
      search$: Observable<string>;
      itemsToShow$: Observable<string[]>;
    
      constructor(
        private router: Router,
      ) {}
    
      ngOnInit() {}
    
      ngAfterViewInit() {
        this.search$ = merge(concat(
           of(''),
           fromEvent(this.searchField.nativeElement, 'keyup')
         )).pipe(
           map(() => this.searchField.nativeElement.value)
         );
        this.itemsToShow$ = this.itemsToShow();
      }
      itemsToShow() {
        let itemAsLocalVar: string[];
        return of(ITEMS).pipe(
          delay(10),
          tap(items => itemAsLocalVar = items),
          switchMap(() => {
            return combineLatest([this.search$]);
          }),
          map(([searchFilter]) => itemAsLocalVar.filter(i => i.includes(searchFilter))),
          tap(i => console.log(i))
        );
      }
      goToList() {
        this.router.navigate(['list']);
      }
    }
    

    非常感谢任何关于出问题的想法。

    任何想法

    最佳答案

    奇怪/怪异的行为,但是很好,您发布了此问题/问题。

    我认为生产模式中的问题正在发生,因为变更检测在生产与开发模式中的工作方式[https://blog.angularindepth.com/a-gentle-introduction-into-change-detection-in-angular-33f9ffff6f10]

    在开发模式下,更改检测运行两次以确保[因为您可能已经看到Expression changed.....异常[https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4]
    另请注意,您正在ngAfterViewInit中设置可观察对象。
    由于有两个变更检测周期,因此在开发人员模式下,您可以看到ListComponent正确呈现。在第一个变更检测周期之后,您已在ngAfterViewInit中分配了可观察到的对象,该变量在第二个变更检测周期中被检测到,并按预期方式呈现了组件。

    在生产模式下,更改检测不会运行两次。由于增强了性能,它只能运行一次。如果再次单击“列表”按钮(在第一次单击后),则ListComponent将呈现列表,因为单击该按钮将运行Angular变化检测。

    要解决此问题,您可以使用以下选项-

    1. 通过注入(inject)以下方式强制进行更改检测:

    constructor(private _cdref: ChangeDetectorRef) {
    
      }
    

    并像这样更改您的ngAfterViewInit()
    ngAfterViewInit() {
        this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
          map(() => this.searchField.nativeElement.value)
        );
        this.itemsToShow$ = this.itemsToShow();
        this._cdref.detectChanges();
      }
    

    2. 像这样将ngAfterViewInit()代码移动到ngOnInit():
    ngOnInit() {
    
        this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
          map(() => this.searchField.nativeElement.value)
        );
        this.itemsToShow$ = this.itemsToShow();
    
      }
    

    我建议选择选项2。

    09-12 15:08