简短而甜蜜的答案是“不要遍历整个数组”,但这对我来说还不够好。我希望它看起来像整个条目列都存在。所以我在上面放了一个分隔符,ngFor 遍历数组的一个子部分,下面还有一个分隔符,这使得列表看起来所有元素一直都在那里。
这是我的 html 的简化版本,其中仅包含与此问题相关的部分 (full example on bitbucket):
<div (scroll)="ColScroll($event)">
<div [style.height]="Math.max(0, Math.max(0, scrollPos - 10) * 132)"></div>
<entry *ngFor="let p of (base.pokemon | filter:search:SelectedVer:SelectedLang) | justafew:scrollPos" [pokemon]="p"></entry>
<div [style.height]="Math.max(0,((base.pokemon | filter:search:SelectedVer:SelectedLang).length - scrollPos - 40)) * 132"></div>
</div>
极简结构,绝对清晰:
<div> <!-- column -->
<div></div> <!-- spacer -->
<entry *ngFor='...'></entry>
<div></div> <!-- spacer -->
</div>
首先,非常关键的一点:<entry> 的高度始终为 120 像素,底部边距为 12 像素,总空间为 132 像素。 CSS 使这一点变得绝对。这适用于我想选择的任何恒定大小,但我特别假设大小正好是 132 像素。
简短的版本是,当您滚动列时,scrollHeight 会确定哪些条目实际上应该出现在屏幕上。如果 ngFor 实际构建的前 10 个元素不在屏幕上,那么第一个可见元素从数字 11 开始。我考虑 4k 屏幕并显示 40 个条目(占用 5280 像素)以确保整个列看起来已满。然后,滚动条看起来正确,我在 40 个条目下方有一个间隔,以强制 div 具有适当的可滚动高度。这是视觉上正在发生的事情的图像:
这是控制器中的相关变量和函数(bitbucket):
scrollPos = 0;
...
ColScroll(event: Event) {
let pos = $(event.target).scrollTop();
this.scrollPos = Math.floor(pos / 132);
}
在这里使用 jQuery 让我很生气,但我已经将它用于其他用途,我需要一些跨浏览器的东西。 scrollPos 保存我应该在屏幕上显示的第一个项目的第一个索引。
实际构建所有 <entry> 元素的 ngFor 如下所示:
*ngFor="let p of (base.pokemon | filter:search:SelectedVer:SelectedLang) | justafew:scrollPos"
分解:
base.pokemon 是创建每个条目元素所需的口袋妖怪数据数组。
... | filter:search:SelectedVer:SelectedLang) 用于搜索列表。我将它留在我的示例中,以表明您仍然可以在我的 hack 发挥作用之前使用该列表。
... | justafew:scrollPos 是魔法发生的地方。这是整个过滤器 (bitbucket):
import { Pipe, PipeTransform } from '@angular/core';
import { MinPokemon } from '../models/base';
@Pipe({
name: 'justafew',
pure: false
})
export class JustAFewPipe implements PipeTransform {
public transform(value: MinPokemon[], start: number): MinPokemon[] {
return value.slice(Math.max(0, start - 10), start + 30);
}
}
scrollPos 作为start 参数传入。例如,如果我将列向下滚动了 13200 像素,则 scrollPos 将设置为 100(请参阅上面控制器中的滚动事件)。这将对数组进行切片,使其返回元素 90 到 130。我想稍微溢出屏幕以确保快速滚动不会导致可见的空白区域(疯狂的快速滚动可能仍会显示它,但你移动得如此之快很容易认为浏览器根本没有渲染得那么快,所以我让它滑动)。我使用Math.max,所以我不会使用负数进行切片,例如当我位于列表的最顶端且scrollPos 为0 时。
现在是垫片。他们保持滚动条诚实。我绑定了他们的[style.height] 并使用了一些数学来使这些垫片占据所需的空间。当我向下滚动时,顶部垫片变高,底部垫片收缩完全相同的量,因此列始终具有相同的高度。当我向上滚动时,数学结果正好相反:顶部缩小,底部增大。底部间隔使用与 ngFor 完全相同的过滤器逻辑,因此如果我运行返回 100 而不是 721 pokemon 的搜索,它会调整为 100 个条目的高度。第一个间隔使用scrollPos - 10,因为justafew 过滤器返回10。出于同样的原因,底部间隔使用scrollPos - 30,因为这是justafew 返回的数量。
我知道它看起来像很多活动部件,但它们都简单快捷。不幸的是,到处都有很多相互依赖的“神奇数字”,但考虑到性能改进和可靠性,这让我展示了整个列表,我让它滑动。也许有一天我会制作一个组件或指令,将它们全部放在一个可配置的地方。
更新:大约 2 1/2 年后,Angular 7 发布 there's now a Angular Material package for virtual scrolling。我在我的网站上创建了a few changes,并在大约一个小时内完成了虚拟滚动。即使是组件回收。我强烈推荐使用 Angular Material 进行虚拟滚动。