一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

ngx-virtual-scroller:实践指南

时间:2026-09-11 09:50:01 编辑:袖梨 来源:一聚教程网

准备试用ngx-virtual-scroller之前,先别急着安装;这个项目提供的是虚拟滚动显示虚拟的“无限”列表。一旦进入日常自动化环节,输入边界、依赖和失败处理如果不清楚就很难稳定复用会直接影响交付,这也是我最关心的风险。我的评估方法是用一项范围明确的真实任务完成最小试跑,然后检查配置时间、输出质量、异常信息和维护痕迹是否与文档一致。它更像给愿意先做小范围验证并复查原始文档的团队准备的可审查方案,是否长期使用应由试跑数据决定。

ngx 虚拟滚动条

虚拟滚动显示虚拟的“无限”列表。支持 horizontal/vertical、可变高度和多列。

由 angular2-virtual-scroll 更名为 ngx-virtual-scroller 。请更新您的_package.json_

关于

该模块显示一小部分记录,刚好足以填充视口,并在用户滚动时使用相同的 DOM 元素。 此方法非常有效,因为无论列表大小如何,DOM 元素的数量始终恒定且很小。因此,虚拟滚动可以有效地显示无限增长的项目列表。

  • 支持多列
  • 易于使用 APIs
  • 开源并可在 GitHub 中使用

重大变化:

  • v3.0.0 删除了几个已弃用的属性(请参阅更改日志)。
    • 如果 items 数组前面添加了其他项目,请在可能的情况下继续滚动当前可见的项目。没有标志可以禁用此功能,因为它似乎在所有情况下都是最佳的用户体验。如果您不同意,请创建问题。
  • v2.1.0 依赖注入语法已更改。
  • v1.0.6 viewPortIndices API 属性已删除。 (使用 viewPortInfo 代替)
  • v1.0.3 将所有内容从 virtual-scroll 重命名为 virtual-scroller,从 virtualScroll 重命名为 virtualScroller
  • v0.4.13 resizeBypassRefreshTheshold 重命名为 resizeBypassRefreshThreshold(拼写错误)
  • v0.4.12 change/start/end 事件的开始值和结束值包括 bufferAmount,这使它们变得混乱。此问题已得到纠正。
    • viewPortIndices.arrayStartIndex 重命名为 viewPortIndices.startIndex,viewPortIndices.arrayEndIndex 重命名为 viewPortIndices.endIndex
  • v0.4.4 IPageInfo.endIndex 的值并不直观。此问题已得到纠正。 IPageInfo.startIndex 和 IPageInfo.endIndex 都是在视口中渲染的项目的从 0 开始的数组索引。 (以前 Change.EndIndex 是数组索引 + 1)

注意 - 标记为 (DEPRECATED) 的 API 方法将在下一个主要版本中删除。请尝试停止在代码中使用它们,如果您认为它们仍然有必要,请创建问题。

新功能:

  • RTL 水平滚动条支持
  • 支持 <table> 元素上的固定 <thead>
  • 添加了 API 来查询当前滚动 px 位置(也作为参数传递给 IPageInfo 侦听器)
  • 添加了 API 以使缓存的子项目测量值无效(如果您的子项目尺寸动态变化)
  • 添加 API 滚动到特定 px 位置
  • 如果滚动容器调整大小,项目将自动刷新。如果它导致任何性能问题,可以通过设置 [checkResizeInterval]="0" 来禁用
  • useMarginInsteadOfTranslate 标志。默认为_false_。这可能会影响性能(better/worse,具体取决于您的情况),并且还会为 transform+position:修复的浏览器错误创建解决方法。
  • 支持水平滚动条
  • 支持不同尺寸的元素
  • 添加了将其他元素放入滚动内部的功能(需要将列表本身包装在 @ContentChild('container') 中)
  • 添加了使用任何带滚动条的父元素而不是此元素的功能(@Input() parentScroll)

演示

请在此处查看演示

用途

首选选项:

<virtual-scroller #scroll [items]="items">
    <my-custom-component *ngFor="let item of scroll.viewPortItems">
    </my-custom-component>
</virtual-scroller>

选项2: 注意:viewPortItems 必须是公共字段才能与 AOT 一起使用

<virtual-scroller [items]="items" (vsUpdate)="viewPortItems = $event">
    <my-custom-component *ngFor="let item of viewPortItems">
    </my-custom-component>
</virtual-scroller>

选项3: 注意:viewPortItems 必须是公共字段才能与 AOT 一起使用


    <my-custom-component *ngFor="let item of viewPortItems">
    </my-custom-component>

开始使用

第 1 步: 安装 ngx-virtual-scroller

npm install ngx-virtual-scroller

第2步: 将虚拟滚动模块导入到您的应用程序模块中

....
import { VirtualScrollerModule } from 'ngx-virtual-scroller';

....

@NgModule({
    ...
    imports: [
        ....
        VirtualScrollerModule
    ],
    ....
})
export class AppModule { }

步骤 3:virtual-scroller 标签包裹在元素周围;

<virtual-scroller #scroll [items]="items">
    <my-custom-component *ngFor="let item of scroll.viewPortItems">
    </my-custom-component>
</virtual-scroller>

您还必须定义容器及其子容器的宽度和高度。

virtual-scroller {
  width: 350px;
  height: 200px;
}

my-custom-component {
  display: block;
  width: 100%;
  height: 30px;
}

步骤 4: 创建 my-custom-component 组件。

my-custom-component 必须是此库之外的自定义 angular 组件。

如果您的项目足够简单,则不需要子组件。见下文。

<virtual-scroller #scroll [items]="items">
    {{item?.name}}
</virtual-scroller>

接口

interface IPageInfo {
	startIndex: number;
	endIndex: number;
	scrollStartPosition: number;
	scrollEndPosition: number;
	startIndexWithBuffer: number;
	endIndexWithBuffer: number;
	maxScrollPosition: number;
}

API

按_字母顺序_排列:

属性 Type & 默认 描述
bufferAmount number enableUnequalChildrenSizes ? 5 : 0 要在当前容器视口上方和下方渲染的元素数量。如果 enableUnequalChildrenSizes 工作得不够好,请增加此值。
checkResizeInterval number 1000 检查 _virtual-scroller_ (或 parentScroll)是否已调整大小的频率(以毫秒为单位)。如果调整大小,它将调用 Refresh() 方法
compareItems Function ===比较 语法 (item1:any, item2:any)=&gt;boolean 的谓词,在修改项目数组时使用,以确定哪些项目已更改(确定 enableUnequalChildrenSizes 是否需要刷新缓存的子尺寸测量值)。
enableUnequalChildrenSizes boolean 如果你想使用“不等大小”子项功能。这并不完美,但希望对于大多数情况来说“足够接近”。
executeRefreshOutsideAngularZone boolean 滚动时禁用完整应用程序 Angular ChangeDetection,这可以提高性能。要求开发人员对任何可能已更改的组件手动执行更改检测。 USE WITH CAUTION - 阅读下面的“性能”部分。
水平的 boolean 滚动条应该是垂直的还是水平的。
invalidateAllCachedMeasurements Function ()=&gt;void - 强制重新测量所有缓存的项目大小。如果为 enableUnequalChildrenSizes===false,则仅重新测量 1 项。
invalidateCachedMeasurementAtIndex Function (index:number)=&gt;void - 强制重新测量缓存项目大小。
invalidateCachedMeasurementForItem Function (item:any)=&gt;void - 强制重新测量缓存项目大小。
项目 任何[] 在虚拟卷轴内构建模板的数据。这与您传递给 ngFor 的数据相同。需要注意的是,当这些数据发生更改时,整个虚拟滚动都会刷新。
modifyOverflowStyleOfParentScroll boolean 正确 如果要阻止 _ngx-virtual-scroller_ 自动将 parentScroll 元素的溢出样式设置更改为“滚动”,请设置为 false。
parentScroll 元素/窗口 元素(或窗口),将有滚动条。该元素必须是 virtual-scroller 的父元素之一
刷新 Function ()=&gt;void - 强制重新渲染视口中的当前项目。
RTL boolean 如果您希望水平滑块支持从右到左的脚本 (RTL),请设置为 true
resizeBypassRefreshThreshold number 5 如果 _virtual-scroller_(或 parentScroll)仅调整很小的量,则在调整大小检查期间要忽略多少像素。
scrollAnimationTime number 750 滚动动画运行的时间(以毫秒为单位)。 0 将完全禁用 tween/animation。
scrollDebounceTime number 0 如果用户快速滚动(出于性能原因),则延迟刷新视口的毫秒数。
scrollInto Function (item:any, alignToBeginning:boolean = true, additionalOffset:number = 0, animationMilliseconds:number = undefined, animationCompletedCallback:()=&gt;void = undefined)=&gt;void - 滚动到项目
scrollThrottlingTime number 0 如果用户快速滚动(出于性能原因),则延迟刷新视口的毫秒数。
scrollToIndex Function (index:number, alignToBeginning:boolean = true, additionalOffset:number = 0, animationMilliseconds:number = undefined, animationCompletedCallback:()=&gt;void = undefined)=&gt;void - 滚动到索引处的项目
scrollToPosition Function (scrollPosition:number, animationMilliseconds:number = undefined, animationCompletedCallback: ()=&gt;void = undefined)=&gt;void - 滚动到 px 位置
scrollbarHeight number 如果你想覆盖自动计算的滚动条高度。这用于在计算要渲染的项目数时确定可视区域的尺寸。
scrollbarWidth number 如果你想覆盖自动计算的滚动条宽度。这用于在计算要渲染的项目数时确定可视区域的尺寸。
ssrChildHeight number 通过 _Angular Universal/Server-Side-Rendering_ 渲染时要使用的项目模板单元格的硬编码高度
ssrChildWidth number 通过 _Angular Universal/Server-Side-Rendering_ 渲染时要使用的项目模板单元格的硬编码宽度
ssrViewportHeight number 1080 如果通过 _Angular Universal/Server-Side-Rendering_ 渲染,则要使用的 _virtual-scroller_(或 [parentScroll])的硬编码可见高度。
ssrViewportWidth number 1920 如果通过 _Angular Universal/Server-Side-Rendering_ 渲染,则要使用的 _virtual-scroller_(或 [parentScroll])的硬编码可见宽度。
stripedTable boolean 如果您使用条带表,则设置为 true。在这种情况下,行将是 added/removed 两两,以保持条带一致。
useMarginInsteadOfTranslate boolean 在许多情况下,翻译速度更快,因为它可以使用 GPU 加速,但如果滚动容器或子元素不使用任何过渡或不透明度,翻译速度可能会更慢。更重要的是,翻译创建了一个新的“包含块”,它打破了位置:固定,因为它将相对于变换而不是窗口。如果您的子元素遇到position:fixed 问题,请打开此标志。
viewPortInfo IPageInfo 允许按需查询当前视口信息而不是事件。
viewPortItems 任何[] 当前渲染到视口的项目数组。
vsChange Event&lt;IPageInfo&gt; 每次 startend 索引或滚动位置更改时都会触发此事件,并发出 IPageInfo
vsEnd Event&lt;IPageInfo&gt; 每次 end 索引更改时都会触发此事件并发出 IPageInfo
vsStart Event&lt;IPageInfo&gt; 每次 start 索引更改时都会触发此事件并发出 IPageInfo
vsUpdate Event&lt;any[]&gt; 每次 startend 索引更改时都会触发此事件,并根据从 startend 的当前滚动位置发出应该可见的项目列表。此事件发出的列表必须与 *ngFor 一起使用,以呈现 &lt;virtual-scroller&gt; 中的实际项目列表
childHeight (DEPRECATED) number 项目模板单元格的最小高度。如果 enableUnequalChildrenSizes 工作得不够好,请使用此选项。 (如果未指定,则默认使用第一个单元格的实际渲染大小。)
childWidth (DEPRECATED) number 项目模板单元格的最小宽度。如果 enableUnequalChildrenSizes 工作得不够好,请使用此选项。 (如果未指定,则默认使用第一个单元格的实际渲染大小。)

注意 - 不带“vs”前缀的事件已被弃用,因为它们可能由于其“冒泡”性质而与本机 DOM 事件发生冲突。见https://github.com/angular/angular/issues/13997

一个示例是,如果 <virtual-scroller> 内的 <input> 元素发出“更改”事件,该事件会冒泡到 virtual-scroller 的(更改)处理程序。使用 vs 前缀将防止这种冒泡冲突,因为目前没有以 vs 为前缀的官方 DOM 事件。

使用父滚动条

如果要使用父元素的滚动条,请将 parentScroll 设置为本机 DOM 元素。


    <virtual-scroller #scroll [items]="items" [parentScroll]="scrollingBlock">
        <input type="search">
        
            <my-custom-component *ngFor="let item of scroll.viewPortItems">
            </my-custom-component>
        
    </virtual-scroller>

如果 parentScroll 是自定义角度组件(而不是本机 HTML 元素,例如 DIV),Angular 会将 #scrollingBlock 变量包装在 ElementRef https://angular.io/api/core/ElementRef 中,在这种情况下,您需要使用.nativeElement 属性获取底层 JavaScript DOM 元素引用。

<custom-angular-component #scrollingBlock>
    <virtual-scroller #scroll [items]="items" [parentScroll]="scrollingBlock.nativeElement">
        <input type="search">
        
            <my-custom-component *ngFor="let item of scroll.viewPortItems">
            </my-custom-component>
        
    </virtual-scroller>
</custom-angular-component>

注意 - 父元素应该定义宽度和高度。

使用窗口的滚动条

如果要使用窗口的滚动条,请设置parentScroll

<virtual-scroller #scroll [items]="items" [parentScroll]="scroll.window">
    <input type="search">
    
        <my-custom-component *ngFor="let item of scroll.viewPortItems">
        </my-custom-component>
    
</virtual-scroller>

尺寸可变的物品

项目_必须_具有固定的高度和宽度,此模块才能完美工作。如果不是,则设置[enableUnequalChildrenSizes]="true"

(DEPRECATED):如果 enableUnequalChildrenSizes 不起作用,您可以将输入 childWidthchildHeight 设置为其最小可能值。您还可以修改 bufferAmount,这会导致在滚动区域的边缘渲染额外的项目。

<virtual-scroller #scroll [items]="items" [enableUnequalChildrenSizes]="true">

    <my-custom-component *ngFor="let item of scroll.viewPortItems">
    </my-custom-component>

</virtual-scroller>

分块加载

每次滚动条到达列表末尾时都会触发事件 vsEnd。您可以使用它在滚动末尾动态加载更多项目。见下文。

import { IPageInfo } from 'ngx-virtual-scroller';
...

@Component({
    selector: 'list-with-api',
    template: `
        <virtual-scroller #scroll [items]="buffer" (vsEnd)="fetchMore($event)">
            <my-custom-component *ngFor="let item of scroll.viewPortItems"> </my-custom-component>
            Loading...
        </virtual-scroller>
    `
})
export class ListWithApiComponent implements OnChanges {

    @Input()
    items: ListItem[];

    protected buffer: ListItem[] = [];
    protected loading: boolean;

    protected fetchMore(event: IPageInfo) {
        if (event.endIndex !== this.buffer.length-1) return;
        this.loading = true;
        this.fetchNextChunk(this.buffer.length, 10).then(chunk => {
            this.buffer = this.buffer.concat(chunk);
            this.loading = false;
        }, () => this.loading = false);
    }

    protected fetchNextChunk(skip: number, limit: number): Promise<ListItem[]> {
        return new Promise((resolve, reject) => {
            ....
        });
    }
}

配 HTML 表

注意 - #header 角度选择器将使 <thead> 元件固定到顶部。如果您希望标题滚动到视图之外,请不要添加 #header 角度元素引用。

<virtual-scroller #scroll [items]="myItems">
    <table>
        <thead #header>
            <th>Index</th>
            <th>Name</th>
            <th>Gender</th>
            <th>Age</th>
            <th>Address</th>
        </thead>
        <tbody #container>
            <tr *ngFor="let item of scroll.viewPortItems">
                <td>{{item.index}}</td>
                <td>{{item.name}}</td>
                <td>{{item.gender}}</td>
                <td>{{item.age}}</td>
                <td>{{item.address}}</td>
            </tr>
        </tbody>
    </table>
</virtual-scroller>

如果孩子的体型发生变化

virtual-scroller 缓存渲染项目的测量值。如果 enableUnequalChildrenSizes===true 则每个项目都会单独测量和缓存。否则,第一个测量项目将用于所有项目。

如果您的项目可以动态更改尺寸,则需要通知 virtual-scroller 重新测量它们。有 3 种方法可以做到这一点:

virtualScroller.invalidateAllCachedMeasurements();
virtualScroller.invalidateCachedMeasurementForItem(item: any);
virtualScroller.invalidateCachedMeasurementAtIndex(index: number);

如果在滚动离开和返回后恢复子视图状态

virtual-scroller 本质上使用 *ngIf 来删除滚动到视图之外的项目。与将所有屏幕外项目保留在 DOM 中相比,这带来了性能优势。

由于 *ngIf,Angular 完全忘记了任何视图状态。如果您的组件能够更改状态,则您的应用程序有责任将该视图状态保留在您自己的数据绑定到组件的对象中。

例如,如果您的子组件可以通过按钮 expand/collapse,则滚动离开和返回很可能会导致扩展状态恢复为默认状态。

要解决此问题,您需要将所有“视图”状态属性存储在变量中并对其进行数据绑定,以便在从 DOM 获取 removed/re-added 时可以将其恢复。

示例:

<virtual-scroller #scroll [items]="items">
    <my-custom-component [expanded]="item.expanded" *ngFor="let item of scroll.viewPortItems">
    </my-custom-component>
</virtual-scroller>

如果容器尺寸发生变化

注意 - 现在应该自动检测到这一点,但是如果需要,“刷新”方法仍然可以强制执行它。

这是使用 setInterval 方法实现的,可能会导致轻微的性能问题。它不应该引人注目,但可以通过 [checkResizeInterval]="0" 禁用

一旦“Resize Observer”(https://wicg.github.io/ResizeObserver/)完全实现,性能将得到改善。

刷新方法(DEPRECATED)

如果在下拉菜单或可折叠菜单中使用虚拟滚动,则虚拟滚动需要知道容器大小何时发生变化。调整容器大小后使用 refresh() 函数(也包括动画时间)。

import { Component, ViewChild } from '@angular/core';
import { VirtualScrollerComponent } from 'ngx-virtual-scroller';

@Component({
    selector: 'rj-list',
    template: `
        <virtual-scroller #scroll [items]="items">
            
                {{i}}: {{item}}
            
        </virtual-scroller>
    `
})
export class ListComponent {

    protected items = ['Item1', 'Item2', 'Item3'];

    @ViewChild(VirtualScrollerComponent)
    private virtualScroller: VirtualScrollerComponent;

    // call this function after resize + animation end
    afterResize() {
        this.virtualScroller.refresh();
    }
}

聚焦一个项目

您可以使用 scrollInto()scrollToIndex() API 滚动到列表中的项目:

import { Component, ViewChild } from '@angular/core';
import { VirtualScrollerComponent } from 'ngx-virtual-scroller';

@Component({
    selector: 'rj-list',
    template: `
        <virtual-scroller #scroll [items]="items">
            
                {{i}}: {{item}}
            
        </virtual-scroller>
    `
})
export class ListComponent {

    protected items = ['Item1', 'Item2', 'Item3'];

    @ViewChild(VirtualScrollerComponent)
    private virtualScroller: VirtualScrollerComponent;

    // call this function whenever you have to focus on second item
    focusOnAnItem() {
        this.virtualScroller.items = this.items;
        this.virtualScroller.scrollInto(items[1]);
    }
}

配置设置的依赖注入

一些默认配置设置可以通过 DI 覆盖,因此您可以全局设置它们,而不是在_virtual-scroller_的每个实例上设置它们。

providers: [
    provide: 'virtual-scroller-default-options', useValue: {
        checkResizeInterval: 1000,
        modifyOverflowStyleOfParentScroll: true,
        resizeBypassRefreshThreshold: 5,
        scrollAnimationTime: 750,
        scrollDebounceTime: 0,
        scrollThrottlingTime: 0,
        stripedTable: false
    }
],

OR

export function vsDefaultOptionsFactory(): VirtualScrollerDefaultOptions {
    return {
        checkResizeInterval: 1000,
        modifyOverflowStyleOfParentScroll: true,
        resizeBypassRefreshThreshold: 5,
        scrollAnimationTime: 750,
        scrollDebounceTime: 0,
        scrollThrottlingTime: 0,
        stripedTable: false
    };
}

providers: [
    provide: 'virtual-scroller-default-options', useFactory: vsDefaultOptionsFactory
],

对项目进行排序

始终确保将项目的不可变副本发送到虚拟滚动,以避免意外行为。在进行非不可变操作(例如排序)时需要小心:

sort() {
  this.items = [].concat(this.items || []).sort()
}

隐藏滚动条

这个 hacky CSS 允许隐藏滚动条,同时仍然允许滚动 mouseWheel/touch/pageUpDownKeys

    // hide vertical scrollbar
    margin-right: -25px;
    padding-right: 25px;

    // hide horizontal scrollbar
    margin-bottom: -25px;
    padding-bottom: 25px;

滚动中的附加元素

如果除了列表本身(e.g.搜索字段)之外,您还想在虚拟滚动中嵌套其他元素,则需要将这些元素包装在角度选择器名称为 #container 的标签中。

<virtual-scroller #scroll [items]="items">
    <input type="search">
    
        <my-custom-component *ngFor="let item of scroll.viewPortItems">
        </my-custom-component>
    
</virtual-scroller>

性能 - TrackBy

virtual-scroller 使用 *ngFor 渲染可见项。当 *ngFor 数组更改时,Angular 使用 trackBy 函数来确定是否应该重用或重新生成循环中的每个组件。

例如,如果 5 个项目可见,并且滚动导致 1 个项目换出,但其他 4 个项目仍然可见,那么 Angular 没有理由从头开始重新生成这 4 个组件,它应该重用它们。

trackBy 函数必须返回数字或字符串作为对象的唯一标识符。

如果*ngFor使用的数组是number[]string[]类型,Angular的默认trackBy函数将自动工作,你不需要做任何额外的事情。

如果 *ngFor 使用的数组的类型为 any[],则必须编写自己的 trackBy 函数。

以下是如何执行此操作的示例:

<virtual-scroller #scroll [items]="myComplexItems">
    <my-custom-component
        [myComplexItem]="complexItem"
        *ngFor="let complexItem of scroll.viewPortItems; trackBy: myTrackByFunction">
    </my-custom-component>
</virtual-scroller>
public interface IComplexItem {
    uniqueIdentifier: number;
    extraData: any;
}

public myTrackByFunction(index: number, complexItem: IComplexItem): number {
    return complexItem.uniqueIdentifier;
}

性能 - ChangeDetection

virtual-scroller 被编码为非常快。如果您的应用程序中滚动速度很慢,则问题出在您的自定义组件代码上,而不是 virtual-scroller 本身。 以下是如何更正代码的说明。这将使您的整个应用程序变得更快,包括_virtual-scroller_。

默认情况下,Angular 中的每个组件都使用 ChangeDetectionStrategy.Default“CheckAlways”策略。这意味着更改检测周期将不断运行,它将检查 EVERY 组件上的 EVERY 数据绑定表达式,以查看是否有任何更改。 这使得程序员更容易编写应用程序代码,但也使应用程序变得非常慢。

如果 virtual-scroller 感觉很慢,一个可能的快速解决方案可以掩盖真正的问题,那就是使用 scrollThrottlingTimescrollDebounceTime APIs。

正确的解决方法是使循环尽可能快,并避免不必要的 ChangeDetection 循环。如果避免数据绑定中的复杂逻辑,周期将会更快。您可以通过将组件转换为使用 ChangeDetectionStrategy.OnPush 来避免不必要的循环。

ChangeDetectionStrategy.OnPush 意味着消费应用程序承担全部责任告诉 Angular 何时运行更改检测,而不是让 Angular 自行解决。例如,virtual-scroller 具有绑定属性 [items]="myItems"。如果您使用 OnPush,则必须在更改 myItems 数组时告诉 Angular,因为它不会自动确定这一点。 OnPush 对于程序员来说编码起来要困难得多。你必须以不同的方式编码:这意味着

  1. 尽可能避免改变任何绑定属性的状态&
  2. 当你改变状态时手动运行变化检测。 OnPush 可以逐个组件地完成,但我建议对应用程序中的 EVERY 组件执行此操作。

如果您的首要任务是使 virtual-scroller 更快,则 OnPush 的最佳候选者将是用作 virtual-scroller 下的子级的所有自定义组件。如果您在 virtual-scroller 下有多个自定义组件的层次结构,则其中的 ALL 需要转换为_OnPush_。

我个人对在整个应用程序中实现 OnPush 的最简单方法的建议:

import { ChangeDetectorRef } from '@angular/core';

public class ManualChangeDetection {
    public queueChangeDetection(): void {
        this.changeDetectorRef.markForCheck(); // marks self for change detection on the next cycle, but doesn't actually schedule a cycle
        this.queueApplicationTick();
    }

    public static STATIC_APPLICATION_REF: ApplicationRef;
    public static queueApplicationTick: ()=> void = Util.debounce(() => {
        if (ManualChangeDetection.STATIC_APPLICATION_REF['_runningTick']) {
            return;
        }

        ManualChangeDetection.STATIC_APPLICATION_REF.tick();
    }, 5);

    constructor(private changeDetectorRef: ChangeDetectorRef) {
    }
}

// note: this portion is only needed if you don't already have a debounce implementation in your app
public class Util {
    public static throttleTrailing(func: Function, wait: number): Function {
        let timeout = undefined;
        let _arguments = undefined;
        const result = function () {
            const _this = this;
            _arguments = arguments;

            if (timeout) {
                return;
            }

            if (wait <= 0) {
                func.apply(_this, _arguments);
            } else {
                timeout = setTimeout(function () {
                    timeout = undefined;
                    func.apply(_this, _arguments);
                }, wait);
            }
        };
        result['cancel'] = function () {
            if (timeout) {
                clearTimeout(timeout);
                timeout = undefined;
            }
        };

        return result;
    }

    public static debounce(func: Function, wait: number): Function {
        const throttled = Util.throttleTrailing(func, wait);
        const result = function () {
            throttled['cancel']();
            throttled.apply(this, arguments);
        };
        result['cancel'] = function () {
            throttled['cancel']();
        };

        return result;
    }
}

public class MyEntryLevelAppComponent
{
    constructor(applicationRef: ApplicationRef) {
        ManualChangeDetection.STATIC_APPLICATION_REF = applicationRef;
    }
}

@Component({
	...
  changeDetection: ChangeDetectionStrategy.OnPush
	...
})
public class SomeRandomComponentWhichUsesOnPush {
    private manualChangeDetection: ManualChangeDetection;
    constructor(changeDetectorRef: ChangeDetectorRef) {
        this.manualChangeDetection = new ManualChangeDetection(changeDetectorRef);
    }

    public someFunctionThatMutatesState(): void {
        this.someBoundProperty = someNewValue;

        this.manualChangeDetection.queueChangeDetection();
    }
}

ManualChangeDetection/Util 类是帮助程序,可以将 copy/pasted 直接添加到您的应用程序中。 MyEntryLevelAppComponentSomeRandomComponentWhichUsesOnPush 的代码是您需要针对特定应用程序进行修改的示例。如果遵循此模式,OnPush 会更容易实现。然而,真正困难的部分是分析所有代码以确定您在“何处”改变状态。不幸的是,没有灵丹妙药,您需要花费大量时间 reading/debugging/testing 您的代码。

性能 - executeRefreshOutsideAngularZone

API 旨在作为性能问题的快速创可贴修复。请阅读上面的其他性能部分,了解解决性能问题的理想方法。

ChangeDetectionStrategy.OnPush 是推荐的策略,因为它提高了整个应用程序的性能,而不仅仅是_虚拟滚动_。然而,ChangeDetectionStrategy.OnPush 很难实现。在您准备好处理 ChangeDetectionStrategy.OnPush 之前,executeRefreshOutsideAngularZone 可能是一种更简单的初始方法。

如果您已为 100% 的组件正确实现了 ChangeDetectionStrategy.OnPush,则 executeRefreshOutsideAngularZone 将不会提供任何性能优势。

如果您还没有这样做,滚动可能会感觉很慢。这是因为 Angular 在滚动时执行完整应用程序更改检测。但是,很可能只有滚动条内的组件实际上需要运行更改检测,因此完整应用程序更改检测周期是多余的。

在这种情况下,您可以使用以下代码获得 free/easy 性能提升:

import { ChangeDetectorRef } from '@angular/core';

public class MainComponent {
    constructor(public changeDetectorRef: ChangeDetectorRef) { }
}
<virtual-scroller
    #scroll
    [items]="items"
    [executeRefreshOutsideAngularZone]="true"
    (vsUpdate)="changeDetectorRef.detectChanges()"
>
    <my-custom-component *ngFor="let item of scroll.viewPortItems">
    </my-custom-component>
</virtual-scroller>

注意 - executeRefreshOutsideAngularZone 将在所有_虚拟滚动_事件期间禁用 Angular ChangeDetection,包括:vsUpdate、vsStart、vsEnd、vsChange。如果更改这些事件处理程序内的任何数据绑定属性,则必须对这些特定组件执行手动更改检测。这可以通过事件处理程序末尾的 changeDetectorRef.detectChanges() 来完成。

注意 - changeDetectorRef 是特定于组件的,因此您需要将其注入到相应组件的构造函数中的私有变量中,然后再调用它来响应 virtual-scroller 事件。

:警告:WARNING - 未能执行手动更改检测来响应 virtual-scroller 事件将导致您的组件在短时间内呈现陈旧的 UI(直到下一个更改检测周期),这将使您的应用程序感觉有错误。

注意 - changeDetectorRef.detectChanges() 将对组件及其所有嵌套子组件执行更改检测。如果多个组件需要运行更改检测来响应 virtual-scroller 事件,您可以从祖先层次结构中的更高级别组件调用 detectChanges,而不是在每个单独的组件上调用。然而,重要的是要避免太多额外的更改检测周期,不要在层次结构中走得太高,除非所有嵌套的子级确实需要执行更改检测。

注意 - 所有 virtual-scroller 事件都会同时发出,以响应其内部“刷新”功能。如果某些条件不适用,其中一些事件发射器将被绕过。但是 vsUpdate 将始终被发出。因此,您应该将所有数据绑定属性更改和手动更改检测合并到 vsUpdate 事件处理程序中,以避免在其他 virtual-scroller 事件期间执行重复的更改检测周期。

在上面的代码示例中,(vsUpdate)="changeDetectorRef.detectChanges()" 是必要的,因为 scroll.viewPortItems 在发出之前在其内部“渲染”函数期间已在内部更改为_virtual-scroller_(vsUpdate)。 executeRefreshOutsideAngularZone 阻止 MainComponent 刷新其数据绑定以响应此更改,因此必须运行手动更改检测周期。 virtual-scroller 或 my-custom-component 不需要额外的手动更改检测代码,即使它们的数据绑定属性已更改,因为它们是 MainComponent 的嵌套子级。

性能 - scrollDebounceTime / scrollThrottlingTime

这些 APIs 旨在作为性能问题的快速创可贴修复。请阅读上面的其他性能部分,了解解决性能问题的理想方法。

如果没有这些设置,virtual-scroller 将在用户滚动时立即刷新。 油门将延迟刷新,直到滚动开始后_#毫秒_。当用户继续滚动时,它将在每次连续刷新之间等待相同的_#毫秒_。即使用户停止滚动,它仍然会在最终刷新之前等待分配的时间。 在用户停止滚动_#毫秒_之前,反跳不会刷新。 如果同时设置了去抖和节流,则去抖优先。

注意 - 如果 virtual-scroller 尚未刷新并且用户已滚动经过 bufferAmount,则不会渲染任何子项,并且 virtual-scroller 将显示为空白。这可能会让用户感到困惑。发生这种情况时,您可能希望显示微调器或加载消息。

Angular 通用/服务器端渲染

初始的 SSR 渲染不是一个功能齐全的站点,它本质上是一个 HTML“屏幕截图”(HTML/CSS,但没有 JS)。但是,一旦在后台下载了完整的应用程序,它就会立即将您的“屏幕截图”替换为真实网站。 SSR 的目的是非常快速地提供正确的视觉效果,因为完整的角度应用程序可能需要很长时间才能下载。这使用户“认为”您的网站速度很快,因为希望他们在功能齐全的网站在后台完成加载之前不会单击任何需要 JS 的内容。此外,它还允许没有 JavaScript 的屏幕抓取工具正常工作(例如:Face@book posts/etc)。

virtual-scroller 依赖 JavaScript APIs 来测量子元素的大小及其父元素的可滚动区域。这些APIs在SSR中不起作用,因为HTML/CSS“屏幕截图”是通过Node在服务器上生成的,它不像浏览器那样在站点上生成execute/render。这意味着 virtual-scroller 会将所有测量值视为未定义,并且将无法正确生成“屏幕截图”。最有可能的是,只有 1 个子元素会出现在您的_virtual-scroller_ 中。这个“屏幕截图”可以用polyfills修复。但是,当浏览器呈现“屏幕截图”时,滚动行为在完整应用程序加载之前仍然无法工作。

SSR 是一个高级(且复杂)的主题,此处无法完全解决。这个请自行研究。不过,这里有一些建议:

  1. main.server.ts 文件中使用 https://www.npmjs.com/package/domino 和 https://www.npmjs.com/package/raf polyfill
const domino = require('domino');
require('raf/polyfill');
const win = domino.createWindow(template);
win['versionNumber'] = 'development';
global['window'] = win;
global['document'] = win.document;
Object.defineProperty(win.document.body.style, 'transform', { value: () => { return { enumerable: true, configurable: true }; } });
  1. 确定要用于 SSR“屏幕截图”计算的默认屏幕尺寸(建议:1920x1080)。这对于所有用户来说并不准确,但希望足够接近。一旦完整的 Angular 应用程序在后台加载,它们的真实设备屏幕尺寸将接管。
  2. 在没有 SSR 的真实浏览器中运行您的应用程序,并确定 virtual-scroller 内子元素的平均值 width/height 以及 virtual-scroller 的 width/height (或 [parentScroll] 元素)。使用这些值设置 [ssrChildWidth]/[ssrChildHeight]/[ssrViewportWidth]/[ssrViewportHeight] 属性。
<virtual-scroller #scroll [items]="items">

    <my-custom-component
        *ngFor="let item of scroll.viewPortItems"
        [ssrChildWidth]="138"
        [ssrChildHeight]="175"
        [ssrViewportWidth]="1500"
        [ssrViewportHeight]="800"
    >
    </my-custom-component>

</virtual-scroller>

已知问题

以下是我们不知道如何解决或没有资源来解决的已知问题。请不要为他们提交票证。如果您知道如何修复它们,请提交拉取请求:slightly_smiling_face:

嵌套滚动条

如果页面上有 2 个嵌套滚动条,则鼠标滚轮只会影响距离当前鼠标位置最近的父级滚动条。这意味着,如果您使用鼠标滚轮滚动到 virtual-scroller 的底部并且窗口有一个额外的滚动条,则无法使用滚轮滚动页面,除非您将鼠标指针移出 virtual-scroller 元素。

版本

检查 CHANGELOG

热门栏目