虚拟化
在 Blazor 框架中,可以使用内置的虚拟化组件 Virtualize<TItem>
提高渲染性能。虚拟化是一种技术,用于将 UI 渲染限制为仅当前可见的部分。 假设当前有一个无限长的列表,并且在任何给定的时间只需要一小部分项可见时,虚拟化很有帮助。
适合使用 Virtualize<TItem>
组件的情况:
- 在循环中渲染一组数据项
- 由于滚动,大多数项不可见
- 渲染的项的大小相同
当用户滚动到 Virtualize<TItem>
组件的项列表中的任意点时,组件将计算要显示的可见项。 看不见的项将不会渲染。如果不使用虚拟化,典型列表可能会使用 C# foreach
循环来渲染列表中的每一项。
一、直接指定列表数据源
如下例所示,如果集合中包含了数千个学生,则渲染这些学生会花费较长时间,并且用户明显会感觉到 UI 的滞后。 而且即使渲染出了全部学生,但是大多数的学生信息都是看不到的,因为它们超出了 <div>
元素的高度。
<div style="height:500px;overflow-y:scroll">@foreach (var student in _students){<p>@student.Name @student.Age</p>}
</div>
实际上,没必要一次性渲染所有学生信息,只需要将能看到的部分渲染出来就足够了,要实现这一点,只需要将上诉例子中的foreach
循环替换为 Virtualize<TItem>
组件。
Virtualize<TItem>
组件会根据容器的高度和渲染的项的大小来计算需要渲染的项数,随着用户滚动,重新计算并重新呈现项。
常用的组件参数
-
Items
:Virtualize<TItem>
组件的组件参数,指定数据源(列表)。 -
Context
:Virtualize<TItem>
组件的组件参数,设置子项对象名称。如果没有设置,那么可以直接使用context
来访问列表的子项。 -
示例
@page "/vitual-test"<h3>VitualTest</h3><div style="height:500px;overflow-y:scroll"><Virtualize Context="student" Items="_students"><p>@student.Name @student.Age</p></Virtualize> </div>@code {private List<Student> _students = new();protected override void OnInitialized(){if (_students.Count == 0){foreach (var i in Enumerable.Range(0, 1000)){_students.Add(new Student{Id = i,Name = "Schuyler" + i,Age = 20 + i});}}base.OnInitialized();}public class Student{public int Id { get; set; }public string Name { get; set; } = null!;public int Age { get; set; }} }
二、指定数据源提供方法
如果不想将所有项加载到内存中或者集合不是泛型 ICollection<T>
,可以使用Virtualize<TItem>
的组件参数 ItemsProvider
指定数据源提供方法,以按需异步检索请求的项。
指定数据源提供方法
ItemsProvider
:Virtualize<TItem>
组件的组件参数,用于指定数据源提供方法。
- 数据源提供方法接收
ItemsProviderRequest
对象参数,该对象存在如下属性:StartIndex
:请求项的起始位置,从0开始。Count
:请求项的个数。CancellationToken
:请求的取消令牌。
- 数据源提供方法在查询到对应的数据后,通过
ItemsProviderResult<TItem>
的形式将这些项与项总数一起返回。
Virtualize<TItem>
组件中,Items
和ItemsProvider
只能使用其中一个。
-
示例
@page "/vitual-test"<h3>VitualTest</h3><div style="height:500px;overflow-y:scroll"><Virtualize Context="student" ItemsProvider="LoadStudents"><p>@student.Name @student.Age</p></Virtualize> </div>@code {private List<Student> _students = new();protected override void OnInitialized(){if (_students.Count == 0){foreach (var i in Enumerable.Range(0, 1000)){_students.Add(new Student{Id = i,Name = "Schuyler" + i,Age = 20 + i});}}base.OnInitialized();}private ValueTask<ItemsProviderResult<Student>> LoadStudents(ItemsProviderRequest request){//一般会从数据库中直接获取数据,这里为了方便,直接从集合中获取var showStudents = _students.Skip(request.StartIndex).Take(request.Count);return ValueTask.FromResult(new ItemsProviderResult<Student>(showStudents, _students.Count()));}public class Student{public int Id { get; set; }public string Name { get; set; } = null!;public int Age { get; set; }} }
重新请求数据源
RefreshDataAsync()
:Virtualize<TItem>
组件对象的异步方法,用于从ItemsProvider
指定的方法中重新请求数据。当基础数据源发生更改时,需要进行调用。
-
RefreshDataAsync()
方法会更新Virtualize<TItem>
组件的数据,但是并不会引发渲染。因此如果是在Blazor的内置事件(例如@onclick
等)的处理方法或组件的生命周期方法中调用RefreshDataAsync()
,不需要再手动触发渲染。否则,需要在RefreshDataAsync()
后调用StateHasChanged()
以更新UI。 -
注意这里说的是
ItemsProvider
指定数据源的方式,如果使用Items
,则不需要调用RefreshDataAsync()
方法。 -
示例
<Virtualize ... @ref="virtualizeComponent">... </Virtualize>...private Virtualize<FetchData>? virtualizeComponent;protected override void OnInitialized() {WeatherForecastSource.ForecastUpdated += async () => {await InvokeAsync(async () =>{await virtualizeComponent?.RefreshDataAsync();StateHasChanged();});}); }
三、占位符
由于获取数据源的过程可能需要一些时间,或者在查询完成后Items
为空或 ItemsProviderResult<TItem>.TotalItemCount
为零时会出现卡顿或不友善的显示,此时可以配合使用Virtualize<TItem>
组件的Placeholder
属性、ItemContent
属性和EmptyContent
属性来设置对应的展示内容。
ItemContent
:Virtualize<TItem>
组件的属性,用于设置数据源加载完成且有数据时,所展示的内容。
Placeholder
:Virtualize<TItem>
组件的属性,用于设置数据源获取过程中,所展示的临时内容。
EmptyContent
:Virtualize<TItem>
组件的属性,用于设置Items
为空或 ItemsProviderResult<TItem>.TotalItemCount
为零时所展示的内容。
-
示例
@page "/empty-content"<PageTitle>Empty Content</PageTitle><h1>Empty Content Example</h1><Virtualize Items="@stringList"><ItemContent><p>@context</p></ItemContent><Placeholder><p>Loading</p></Placeholder><EmptyContent><p>There are no strings to display.</p></EmptyContent> </Virtualize>@code {private List<string>? stringList;protected override void OnInitialized() =>stringList ??= new() { "Here's a string!", "Here's another string!" }; }
四、其他设置
1、项大小
每个项的像素高度可以使用Virtualize<TItem>
组件的ItemSize
属性进行设置(默认值50pix)。
Virtualize<TItem>
组件在初始渲染执行后测量各个项的渲染大小(高度)。 使用ItemSize
来提前提供准确的项大小,可以帮助实现准确的初始渲染性能,并确保页面重载时处于正确滚动位置。 如果 ItemSize
设置不正确,将会导致某些项在当前可见区域之外显示,会触发第二次重新渲染,严重时会导致数据项闪烁。
- 尝试后发现
ItemSize
的值不会影响可见区域内显示数据的个数,只会影响获取数据时的总个数 - 如果
ItemSize
设置过大,会导致某些项在当前可见区域之外显示,此时多次渲染列表
<Virtualize Context="student" ItemsProvider="LoadStudents" ItemSize="30"><div style="height:30px;">@student.Name @student.Age</div>
</Virtualize>
2、溢出扫描计数
Virtualize<TItem>
组件的OverscanCount
属性可以设置在可见区域之前和之后渲染的额外项数。 此设置有助于降低滚动期间的渲染频率。 但是,值越大,页面中渲染的元素越多(默认值:3)。
<Virtualize Context="employee" Items="employees" OverscanCount="4">...
</Virtualize>
3、键盘滚动支持
若要允许用户使用键盘滚动虚拟化内容,请确保虚拟化元素或滚动容器本身可聚焦。 如果无法执行此步骤,则键盘滚动在基于 Chromium的浏览器中不起作用。
可在滚动容器元素上使用 tabindex
属性:
-1
:表示元素是可聚焦的,但是不能通过键盘导航来访问到该元素0
:表示元素是可聚焦的,并且可以通过键盘导航来聚焦到该元素,它的相对顺序是当前处于的 DOM 结构来决定的正值
:表示元素是可聚焦的,并且可以通过键盘导航来访问到该元素;它的相对顺序按照tabindex
的数值递增而滞后获焦
<div style="height:500px; overflow-y:scroll" tabindex="-1"><Virtualize Items="allFlights"><div class="flight-info">...</div></Virtualize>
</div>
高阶用法
一、间隔元素与高级样式
根据设计,Virtualize<TItem>
组件仅支持特定的元素布局机制,下面了解一下Virtualize<TItem>
组件在浏览器中渲染的DOM结构以及渲染过程。便于我们了解哪些元素布局可正常工作。
-
源代码
<div style="height:500px; overflow-y:scroll" tabindex="-1"><Virtualize Items="allFlights" ItemSize="100"><div class="flight-info">Flight @context.Id</div></Virtualize> </div>
-
渲染后的DOM结构
<div style="height:500px; overflow-y:scroll" tabindex="-1"><div style="height:1100px"></div><div class="flight-info">Flight 12</div><div class="flight-info">Flight 13</div><div class="flight-info">Flight 14</div><div class="flight-info">Flight 15</div><div class="flight-info">Flight 16</div><div style="height:3400px"></div> </div>
渲染的实际行数和间隔的大小样式和 Items
集合大小而异。 但是,可以观察到虚拟化的内容之前和之后注入了间隔 <div>
元素。 这两个间隔元素的作用如下:
- 在可见内容的前后提供高度偏移量,该偏移量根据滚动情况而动态变化,以确保当前显示的内容在滚动范围内呈现于正确的位置
- 间隔元素代表了所有内容的总高度
- 需要监测用户何时滚动超出当前可见范围,以便及时渲染不同的内容,并发出请求新数据的指令
间隔元素在内部使用交集观察程序,以在其变得可见时接收通知, Virtualize
取决于是否接收这些事件。
- 交集观察程序:交叉观察器是浏览器提供的WebAPI,提供了一种异步检测目标元素与祖先元素或顶级文档的视口相交情况变化的方法。
Virtualize
的运行条件
基于上文所述,Virtualize
的运行条件如下:
- 所有渲染的内容项(包括占位符内容)的高度相同。 这样就可计算哪项内容对应于给定的滚动位置,而无需先获取每个数据项和将数据渲染到 DOM 元素中。
- 间隔和内容行都渲染在单个垂直堆栈中,每个项填充整个水平宽度。 这通常是默认设置, 如果使用 CSS 创建更高级的布局,请记住以下要求:
- 滚动容器样式需要具有以下值之一的
display
:block
(div
的默认值)。table-row-group
(tbody
的默认值)。flex
,其中flex-direction
设置为column
。 确保Virtualize<TItem>
组件的直接子级不会在弹性规则下收缩。 例如,添加.mycontainer > div { flex-shrink: 0 }
- 内容行样式需要具有以下值之一的
display
:block
(div
的默认值)。table-row
(tr
的默认值)。
- 滚动容器样式需要具有以下值之一的
请勿使用 CSS 干扰间隔元素的布局。 默认情况下,间隔元素的 display
值为 block
,除非父元素是表行组。在表行组中,它们默认为 table-row
。 不要试图影响间隔元素的宽度或高度,包括使它们具有边框或 content
伪元素。阻止间隔和内容元素渲染为单个垂直堆栈或导致内容项高度变化的任何方法都会阻止 Virtualize<TItem>
组件正常运行。
二、设置间隔元素
默认情况下,Virtualize<TItem>
组件中的间隔元素为 div
,如果在需要特定子元素的元素中使用 Virtualize<TItem>
组件,可以通过 SpacerElement
属性获取或设置虚拟化间隔元素。
如下面例子中所示,Virtualize<TItem>
组件在表主体元素 (tbody
) 中使用,默认情况下间隔元素为 div
。
实际上,tbody
的子元素应该为相应子元素 tr
,因此使用 SpacerElement
设置为 tr
-
示例
<table id="virtualized-table">......<tbody><Virtualize Items="fixedItems" ItemSize="30" SpacerElement="tr">......</Virtualize></tbody> </table>
三、根级虚拟化
Virtualize<TItem>
组件支持根虚拟化,其会自动向上检测是否有 overflow-y: scroll
样式的父辈元素:
- 如果有,它会使用根级虚拟化来优化渲染和更新性能
- 如果没有,
Virtualize<TItem>
组件会退回到普通的虚拟化渲染方式,不会使用根级虚拟化。
同时,Virtualize<TItem>
组件支持将文档本身用作滚动根,来代替使用具有 overflow-y: scroll
的某些其他元素。
下面例子中,就使用html
和body
元素作为滚动根,这里只是为了演示可行性,实际上,只需要使用html
或body
其中一种就可以了。
<HeadContent><style>html, body { overflow-y: scroll }</style>
</HeadContent>
-
完整示例-VirtualizedTable.razor
@page "/virtualized-table" @rendermode InteractiveServer<PageTitle>Virtualized Table</PageTitle><HeadContent><style>html{overflow-y: scroll}</style> </HeadContent><h1>Virtualized Table Example</h1><table id="virtualized-table"><thead style="position: sticky; top: 0; background-color: silver"><tr><th>Item</th><th>Another column</th></tr></thead><tbody><Virtualize Items="fixedItems" ItemSize="30" SpacerElement="tr"><tr @key="context" style="height: 30px;" id="row-@context" ><td>Item @context</td><td>Another value</td></tr></Virtualize></tbody> </table>@code {private List<int> fixedItems = Enumerable.Range(0, 1000).ToList(); }