从Observable中取N值,直到基于事件完成。延迟加载多选列表

问题描述 投票:3回答:2

我是rxjs的新手,我正在开发一个角度多选列表组件,它应该呈现一长串值(500+)。我正在基于UL渲染列表,我正在迭代一个可以渲染LI的observable。我正在考虑通过一次渲染所有元素来避免影响性能的选项。但我不知道这是否可行,如果可能的话,最好的运营商是什么。

建议的解决方案:

  • 在init上,我将所有数据加载到Observable中。 (src)我将从它获取100个元素并将它们放在目标observable(将用于呈现列表的那个)上
  • 每当用户到达列表末尾(scrollEnd事件触发)时,我将加载100个元素,直到src observable中没有更多值。
  • 目标可观察量中的新值的发射将由scrollEnd事件触发。

在下面找到我的代码,我仍然需要实现建议的解决方案,但我仍然坚持这一点。

编辑:我正在实施@martin解决方案,但我仍然无法在我的代码中使其工作。我的第一步是在代码中复制它,以获取记录的值,但是observable立即完成而不产生任何值。我没有触发事件,而是添加了一个主题。每次scrollindEnd输出发出时,我都会向主题推送一个新值。该模板已被修改以支持此功能。

multiselect.component.ts

import { Component, AfterViewInit } from '@angular/core';
import { zip, Observable, fromEvent, range } from 'rxjs';
import { map, bufferCount, startWith, scan } from 'rxjs/operators';
import { MultiSelectService, ProductCategory } from './multiselect.service';

@Component({
  selector: 'multiselect',
  templateUrl: './multiselect.component.html',
  styleUrls: ['./multiselect.component.scss']
})
export class MultiselectComponent implements AfterViewInit {

  SLICE_SIZE = 100;
  loadMore$: Observable<Event>;
  numbers$ = range(450);

  constructor() {}


  ngAfterViewInit() {
    this.loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');

    zip(
      this.numbers$.pipe(bufferCount(this.SLICE_SIZE)),
      this.loadMore$.pipe(),
    ).pipe(
      map(results => console.log(results)),
    ).subscribe({
      next: v => console.log(v),
      complete: () => console.log('complete ...'),
    });
  }

}

multiselect.component.html

<form action="#" class="multiselect-form">
  <h3>Categories</h3>
  <input type="text" placeholder="Search..." class="multiselect-form--search" tabindex="0"/>
  <multiselect-list [categories]="categories$ | async" (scrollingFinished)="lazySubject.next($event)">
  </multiselect-list>
  <button class="btn-primary--large">Proceed</button>
</form>

多选,list.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'multiselect-list',
  templateUrl: './multiselect-list.component.html'
})
export class MultiselectListComponent {
  @Output() scrollingFinished = new EventEmitter<any>();
  @Input() categories: Array<string> = [];

  constructor() {}

  onScrollingFinished() {
    this.scrollingFinished.emit(null);
  }
}

多选,list.component.html

<ul class="multiselect-list" (scrollingFinished)="onScrollingFinished($event)">
  <li *ngFor="let category of categories; let idx=index" scrollTracker class="multiselect-list--option">
    <input type="checkbox" id="{{ category }}" tabindex="{{ idx + 1 }}"/>
    <label for="{{ category }}">{{ category }}</label>
  </li>
</ul>

注意:scrolllingFinished事件由包含跟踪逻辑的scrollTracker指令触发。我正在将事件从multiselect-list冒泡到multiselect组件。

提前致谢!

javascript angular ecmascript-6 rxjs lazy-loading
2个回答
3
投票

此示例生成一个包含450个项目的数组,然后将它们拆分为100块。它首先转储前100个项目,然后在每个按钮单击后需要另一个100并将其附加到之前的结果。加载所有数据后,此链正确完成。

我认为你应该能够采取这个并用于解决你的问题。只需按下按钮,就可以使用每次用户滚动到底部时都会发出的Subject

import { fromEvent, range, zip } from 'rxjs'; 
import { map, bufferCount, startWith, scan } from 'rxjs/operators';

const SLICE_SIZE = 100;

const loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');
const data$ = range(450);

zip(
  data$.pipe(bufferCount(SLICE_SIZE)),
  loadMore$.pipe(startWith(0)),
).pipe(
  map(results => results[0]),
  scan((acc, chunk) => [...acc, ...chunk], []),
).subscribe({
  next: v => console.log(v),
  complete: () => console.log('complete'),
});

现场演示:https://stackblitz.com/edit/rxjs-au9pt7?file=index.ts

如果你担心性能,你应该使用trackBy for *ngFor来避免重新渲染现有的DOM元素,但我想你已经知道了。


1
投票

这是一个live demo on Stackblitz

如果您的组件订阅了一个包含要显示的整个列表的observable,那么您的服务必须保留整个列表并在每次添加项目时发送一个新列表。这是使用此模式的实现。由于列表是通过引用传递的,因此在observable中推送的每个列表只是一个引用而不是列表的副本,因此发送新列表并不是一项昂贵的操作。

对于服务,使用BehaviorSubject将新项目注入您的可观察对象。你可以使用它的asObservable()方法从中获得一个observable。使用其他属性来保存当前列表。每次调用loadMore()时,按下列表中的新项目,然后将该列表推送到主题中,这也会将其推送到observable中,并且您的组件将重新呈现。

在这里,我开始列出包含所有项目(allCategories)的列表,每次调用loadMore()时,如果采用并使用Array.splice()放置在当前列表中的100个项目块:

@Injectable({
  providedIn: 'root'
})
export class MultiSelectService {
  private categoriesSubject = new BehaviorSubject<Array<string>>([]);
  categories$ = this.categoriesSubject.asObservable();
  categories: Array<string> = [];
  allCategories: Array<string> = Array.from({ length: 1000 }, (_, i) => `item #${i}`);

  constructor() {
    this.getNextItems();
    this.categoriesSubject.next(this.categories);
  }

  loadMore(): void {
    if (this.getNextItems()) {
      this.categoriesSubject.next(this.categories);
    }
  }

  getNextItems(): boolean {
    if (this.categories.length >= this.allCategories.length) {
      return false;
    }
    const remainingLength = Math.min(100, this.allCategories.length - this.categories.length);
    this.categories.push(...this.allCategories.slice(this.categories.length, this.categories.length + remainingLength));
    return true;
  }
}

然后在达到底部时从loadMore()组件调用服务上的multiselect方法:

export class MultiselectComponent {
  categories$: Observable<Array<string>>;

  constructor(private dataService: MultiSelectService) {
    this.categories$ = dataService.categories$;
  }

  onScrollingFinished() {
    console.log('load more');
    this.dataService.loadMore();
  }
}

在你的multiselect-list组件中,将scrollTracker指令放在包含ul而不是li上:

<ul class="multiselect-list" scrollTracker (scrollingFinished)="onScrollingFinished()">
  <li *ngFor="let category of categories; let idx=index"  class="multiselect-list--option">
    <input type="checkbox" id="{{ category }}" tabindex="{{ idx + 1 }}"/>
    <label for="{{ category }}">{{ category }}</label>
  </li>
</ul>

为了检测滚动到底部并仅触发事件一次,使用此逻辑来实现scrollTracker指令:

@Directive({
  selector: '[scrollTracker]'
})
export class ScrollTrackerDirective {
  @Output() scrollingFinished = new EventEmitter<void>();

  emitted = false;

  @HostListener("window:scroll", [])
  onScroll(): void {
    if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight && !this.emitted) {
      this.emitted = true;
      this.scrollingFinished.emit();
    } else if ((window.innerHeight + window.scrollY) < document.body.offsetHeight) {
      this.emitted = false;
    }
  }
}

希望有所帮助!

© www.soinside.com 2019 - 2024. All rights reserved.