从可观察中获取 N 个值,直到基于事件完成.延迟加载多选列表



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

建议的解决方案:

  • 在 init 上,我将所有数据加载到可观察对象中。(src),我将从中取出 100 个元素,并将它们放在目标可观察对象上(将用于渲染列表的那个)
  • 每次用户到达列表末尾(scrollEnd 事件触发)时,我都会再加载 100 个元素,直到 src 中没有更多可观察的值。

  • 目标可观察量中新值的发射将由 scrollEnd 事件触发。

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

编辑:我正在实现@martin解决方案,但我仍然无法使它在我的代码中工作。我的第一步是在代码中复制它,以获取记录的值,但可观察量立即完成而不产生任何值。 我没有触发事件,而是添加了一个主题。每次 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>

multiselect-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);
}
}

multiselect-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>

注意:滚动完成事件由保存跟踪逻辑的 scrollTracker 指令触发。我正在将事件从多选列表冒泡到多选组件。

提前感谢!

此示例生成一个包含 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进行*ngFor以避免重新渲染现有的 DOM 元素,但我想您已经知道这一点。

这是Stackblitz上的现场演示。

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

对于服务,请使用BehaviorSubject在可观察量中注入新项目。您可以使用其asObservable()方法从中获取可观察量。使用其他属性来保存当前列表。每次调用loadMore()时,推送列表中的新项,然后在主题中推送此列表,主题中也会将其推送到可观察量中,组件将重新呈现。

在这里,我从一个包含所有项目(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;
}
}

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

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;
}
}
}

希望对您有所帮助!

最新更新