首页 > 解决方案 > 三态复选框的支持字段被(重新)评估得太频繁

问题描述

我有一个用于多行、la GMail 和类似应用程序的三态复选框选择的“天真”实现。您可以选择单个行,并且有一个顶级三态复选框,指示:

  1. 状态“检查所有行”
  2. 状态“检查了一些但不是所有行”(中级)
  3. 状态“检查了零行”

我说“天真”,因为支持该顶级复选框的字段经常被重新评估,我觉得我可能需要SubjectObservable字段来支持它。

这是我当前实现的重现。

  1. ng new obstest --minimal(角度 5 CLI)
  2. cd obstest
  3. ng generate service search并将其添加到app.module providers
  4. 将此模拟方法添加到服务中:

    search(query: Observable<string>) {
      // Fake search, ignore query for demo
      return of<any[]>([
        { isSelected: false, id: 1, txt: 'item 1' },
        { isSelected: false, id: 2, txt: 'item 2' },
        { isSelected: false, id: 3, txt: 'item 3' },
      ]);
    }
    

    通常这将使用HttpClient从搜索 API 端点获取结果。

  5. 将此添加到app.component.ts文件中:

    enum TriState {
      NothingSelected = '[ ]',
      IntermediateSelection = '[-]',
      EverythingSelected = '[X]',
    }
    
  6. 将组件的装饰更改为:

    @Component({
      selector: 'app-root',
      template: `
        <div><input (keyup)="query$.next($event.target.value)"></div>
        <div (click)="onMultiSelectChange()">{{selectionState}}</div>
        <ul *ngFor="let item of results">
          <li (click)='item.isSelected = !item.isSelected'>
            {{item.isSelected ? '[X]' : '[ ]'}} {{item.txt}}
          </li>
        </ul>`,
    })
    
  7. 用这个替换组件的代码:

    export class AppComponent {
      results: any[];
      query$ = new Subject<string>();
    
      public get selectionCount() {
        console.warn('Getting count at', new Date().toISOString());
        return this.results.filter(r => r.isSelected).length;
      }
    
      public get selectionState() {
        console.warn('Getting state at', new Date().toISOString());
        if (this.selectionCount === 0) { return TriState.NothingSelected; }
        if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; }
        return TriState.IntermediateSelection;
      }
    
      constructor (service: SearchService) { 
        service.search(of('fake query')).subscribe(r => this.results = r);
      }
    
      onMultiSelectChange() {
        if (this.selectionState === TriState.EverythingSelected) {
          this.results.forEach(r => r.isSelected = false);
        } else {
          this.results.forEach(r => r.isSelected = true);
        }
      }
    }
    
  8. import每个文件中的相关内容

  9. ng serve --open

(重新)加载应用程序并打开控制台窗口,然后:

在 KnockoutJS 中,我知道如何使用“计算的 observables”(可能是计算的)来做到这一点,而且我确信这可以使用 Angular 5+ 来完成(可能在 rxjs 的帮助下?)。我只是不确定如何。

我将如何更改selectionCount以及selectionState视图可以数据绑定到它们的方式,但仅在需要时(重新)评估它们?

谁能告诉我惯用的 Angular 和/或 RxJs 解决方案?

标签: angularrxjs

解决方案


this.results从 开始null,因此它在生命周期中有两个分配:首先null是 ,然后是[ ... mock data ... ]您提供的数组。

调查你的吸气剂:

  public get selectionCount() {
    console.warn('Getting count at', new Date().toISOString());
    return this.results.filter(r => r.isSelected).length;
  }

  public get selectionState() {
    console.warn('Getting state at', new Date().toISOString());
    if (this.selectionCount === 0) { return TriState.NothingSelected; }
    if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; }
    return TriState.IntermediateSelection;
  }

selectionState被调用时,它会调用一个警告,然后调用selectionCount两次,因此每次调用selectionState. Angular 不会对 getter 进行任何缓存。由于 的两个分配,它们在整个生命周期中被调用两次this.results,这占负载警告的六个。我不确定剩下的两个是从哪里来的。

编写此类的一种更 RxJS 的方式是避免状态突变并使用可观察的方式完成所有操作,例如:

export class AppComponent {
  results$: Observable<any[]>;
  selections$ = new BehaviorSubject<boolean[]>([]);
  selectionCount$: Observable<number>;
  selectionState$: Observable<TriState>;
  query$ = new Subject<string>();

  constructor (service: SearchService) { 
    this.results$ = service.search(of('fake query')).pipe(shareReplay(1));
    this.selectionCount$ = combineLatest(this.results$, this.selections$).pipe(
       map(([results, selections]) => results.filter((result, i) => selections[i])),
       map(results => results.length),
    );
    this.selectionState$ = of(TriState.IntermediateSelection).pipe(concat(this.results.pipe(map(
      results => {
          if (this.selectionCount === 0) { return TriState.NothingSelected; }
          if (this.selectionCount === this.results.length) { return TriState.EverythingSelected; }
      }))));
  }

  toggle(i) {
    selections$.value[i] = !selections$.value[i];
    selections$.next(selections$.value);
  }

  toggleAll() {
    combineLatest(this.selectionState$, this.results$).pipe(
      first(),
      map(([state, results]) => {
        return results.map(() => state === TriState.EverythingSelected);
      }))
      .subscribe(this.selections$);
  }
}

上面可能有错误,我没有测试它,但希望它传达了这个想法。对于模板,您必须使用| async管道,例如:

@Component({
  selector: 'app-root',
  template: `
    <div><input (keyup)="query$.next($event.target.value)"></div>
    <div (click)="toggleAll()">{{selectionState | async}}</div>
    <ul *ngFor="let item of results | async">
      <li (click)='toggle($index)'>
        {{item.isSelected ? '[X]' : '[ ]'}} {{item.txt}}
      </li>
    </ul>`,
})

不幸的是,Angular 没有像 Redux 那样提供任何标准化的状态管理来强制执行这种模式,所以你要么必须有足够的纪律才能自己做,要么可以接受额外的调用。

或者,您也可以有一个包装器组件来处理Observable相关的状态并且没有模板,并且让子组件只呈现状态。这将避免所有状态转换,您只需要async观察结果即可。我认为这被称为重/轻组件模式?这是一种非常流行的模式,可以避免到处处理 observables,但是我认为我的名字弄错了。


推荐阅读