ASP.NET Core Blazor 高级方案(呈现器树构造)
注意
此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
本文介绍使用 RenderTreeBuilder 手动构建 Blazor 呈现器树的高级方案。
警告
使用 RenderTreeBuilder 创建组件是一种高级方案。 格式不正确的组件(例如,未封闭的标记标签)可能导致未定义的行为。 未定义的行为包括内容呈现损坏、应用功能丢失和安全性受损。
手动构建呈现器树 (RenderTreeBuilder
)
RenderTreeBuilder 提供用于操作组件和元素的方法,包括在 C# 代码中手动生成组件。
以下面的 PetDetails
组件为例,此组件可通过手动方式在另一个组件中呈现。
PetDetails.razor
:
<h2>Pet Details</h2>
<p>@PetDetailsQuote</p>
@code
{
[Parameter]
public string? PetDetailsQuote { get; set; }
}
在以下 BuiltContent
组件中,CreateComponent
方法中的循环生成三个 PetDetails
组件。
在具有序列号的 RenderTreeBuilder 方法中,序列号是源代码行号。 Blazor 差分算法依赖于对应于不同代码行(而不是不同调用的调用)的序列号。 使用 RenderTreeBuilder 方法创建组件时,请对序列号的参数进行硬编码。 通过计算或计数器生成序列号可能导致性能不佳。 有关详细信息,请参阅序列号与代码行号相关,而不与执行顺序相关部分。
BuiltContent.razor
:
@page "/built-content"
<PageTitle>Built Content</PageTitle>
<h1>Built Content Example</h1>
<div>
@CustomRender
</div>
<button @onclick="RenderComponent">
Create three Pet Details components
</button>
@code {
private RenderFragment? CustomRender { get; set; }
private RenderFragment CreateComponent() => builder =>
{
for (var i = 0; i < 3; i++)
{
builder.OpenComponent(0, typeof(PetDetails));
builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
builder.CloseComponent();
}
};
private void RenderComponent() => CustomRender = CreateComponent();
}
@page "/built-content"
<PageTitle>Built Content</PageTitle>
<h1>Built Content Example</h1>
<div>
@CustomRender
</div>
<button @onclick="RenderComponent">
Create three Pet Details components
</button>
@code {
private RenderFragment? CustomRender { get; set; }
private RenderFragment CreateComponent() => builder =>
{
for (var i = 0; i < 3; i++)
{
builder.OpenComponent(0, typeof(PetDetails));
builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
builder.CloseComponent();
}
};
private void RenderComponent() => CustomRender = CreateComponent();
}
@page "/built-content"
<h1>Build a component</h1>
<div>
@CustomRender
</div>
<button @onclick="RenderComponent">
Create three Pet Details components
</button>
@code {
private RenderFragment? CustomRender { get; set; }
private RenderFragment CreateComponent() => builder =>
{
for (var i = 0; i < 3; i++)
{
builder.OpenComponent(0, typeof(PetDetails));
builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
builder.CloseComponent();
}
};
private void RenderComponent()
{
CustomRender = CreateComponent();
}
}
警告
Microsoft.AspNetCore.Components.RenderTree 中的类型允许处理呈现操作的结果。 这些是 Blazor 框架实现的内部细节。 这些类型应视为不稳定,并且在未来版本中可能会有更改。
序列号与代码行号相关,而不与执行顺序相关
Razor 组件文件 (.razor
) 始终被编译。 与解释代码相比,执行编译的代码具有潜在优势,因为生成编译代码的编译步骤可用于注入信息,从而在运行时提高应用性能。
这些改进的关键示例涉及序列号。 序列号向运行时指示哪些输出来自哪些不同的已排序代码行。 运行时使用此信息在线性时间内生成高效的树上差分,这比常规树上差分算法通常可以做到的速度快得多。
以下面的 Razor 组件文件 (.razor
) 为例:
@if (someFlag)
{
<text>First</text>
}
Second
前面的 Razor 标记和文本内容编译为如下所示的 C# 代码:
if (someFlag)
{
builder.AddContent(0, "First");
}
builder.AddContent(1, "Second");
当代码第一次执行且 someFlag
为 true
时,生成器会收到下表中的序列。
序列 | 类型 | 数据 |
---|---|---|
0 | Text 节点 | First |
1 | Text 节点 | 秒 |
假设 someFlag
变为 false
且标记再次呈现。 这一次,生成器会收到下表中的序列。
序列 | 类型 | 数据 |
---|---|---|
1 | Text 节点 | 秒 |
当运行时执行差分时,它会看到序列 0
处的项目已被删除,因此,它会通过单步执行生成以下普通编辑脚本:
- 删除第一个文本节点。
以编程方式生成序列号的问题
想象一下,你编写了以下呈现树生成器逻辑:
var seq = 0;
if (someFlag)
{
builder.AddContent(seq++, "First");
}
builder.AddContent(seq++, "Second");
第一个输出反映在下表中。
序列 | 类型 | 数据 |
---|---|---|
0 | Text 节点 | First |
1 | Text 节点 | 秒 |
此结果与之前的示例相同,因此不存在负面问题。 在第二个呈现中,someFlag
为 false
,输出如下表所示。
序列 | 类型 | 数据 |
---|---|---|
0 | Text 节点 | 秒 |
这次,差分算法看到已发生两个更改。 此算法将生成以下编辑脚本:
- 将第一个文本节点的值更改为
Second
。 - 删除第二个文本节点。
生成序列号会丢失有关原始代码中 if/else
分支和循环的位置的所有有用信息。 这会导致两倍于之前长度的差异。
这是一个普通示例。 在具有深度嵌套的复杂结构(尤其是带有循环)的更真实的情况下,性能成本通常会更高。 差分算法必须深入递归到呈现树中,而不是立即确定已插入或删除的循环块或分支。 这通常导致生成更长的编辑脚本,因为差分算法获知了关于新旧结构之间关系的错误信息。
指南和结论
- 如果动态生成序列号,则应用性能会受到影响。
- 由于缺少必要的信息,该框架无法在运行时自动生成序列号,除非在编译时捕获到了这些信息。
- 不要编写手动实现的冗长 RenderTreeBuilder 逻辑块。 优先使用
.razor
文件并允许编译器处理序列号。 如果无法避免 RenderTreeBuilder 手动逻辑,请将较长的代码块拆分为封装在 OpenRegion/CloseRegion 调用中的较小部分。 每个区域都有自己的独立序列号空间,因此可在每个区域内从零(或任何其他任意数)重新开始。 - 如果序列号已硬编码,则差分算法仅要求序列号的值增加。 初始值和间隔不相关。 一个合理选择是使用代码行号作为序列号,或者从零开始并以 1 或 100 的间隔(或任何首选间隔)增加。
- 对于循环,序列号应在源代码中增加,而不是根据运行时行为来确定。 在运行时,数字重复的事实是差异系统意识到你处于循环中的方式。
- Blazor 使用序列号,而其他树上差分 UI 框架不使用它们。 使用序列号时,差分速度要快得多,并且 Blazor 的优势在于编译步骤可为编写
.razor
文件的开发人员自动处理序列号。