ASP.NET Core Blazor 高级方案(呈现器树构造)

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

本文介绍使用 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");

当代码第一次执行且 someFlagtrue 时,生成器会收到下表中的序列。

序列 类型 数据
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 节点

此结果与之前的示例相同,因此不存在负面问题。 在第二个呈现中,someFlagfalse,输出如下表所示。

序列 类型 数据
0 Text 节点

这次,差分算法看到已发生两个更改。 此算法将生成以下编辑脚本:

  • 将第一个文本节点的值更改为 Second
  • 删除第二个文本节点。

生成序列号会丢失有关原始代码中 if/else 分支和循环的位置的所有有用信息。 这会导致两倍于之前长度的差异。

这是一个普通示例。 在具有深度嵌套的复杂结构(尤其是带有循环)的更真实的情况下,性能成本通常会更高。 差分算法必须深入递归到呈现树中,而不是立即确定已插入或删除的循环块或分支。 这通常导致生成更长的编辑脚本,因为差分算法获知了关于新旧结构之间关系的错误信息。

指南和结论

  • 如果动态生成序列号,则应用性能会受到影响。
  • 由于缺少必要的信息,该框架无法在运行时自动生成序列号,除非在编译时捕获到了这些信息。
  • 不要编写手动实现的冗长 RenderTreeBuilder 逻辑块。 优先使用 .razor 文件并允许编译器处理序列号。 如果无法避免 RenderTreeBuilder 手动逻辑,请将较长的代码块拆分为封装在 OpenRegion/CloseRegion 调用中的较小部分。 每个区域都有自己的独立序列号空间,因此可在每个区域内从零(或任何其他任意数)重新开始。
  • 如果序列号已硬编码,则差分算法仅要求序列号的值增加。 初始值和间隔不相关。 一个合理选择是使用代码行号作为序列号,或者从零开始并以 1 或 100 的间隔(或任何首选间隔)增加。
  • 对于循环,序列号应在源代码中增加,而不是根据运行时行为来确定。 在运行时,数字重复的事实是差异系统意识到你处于循环中的方式。
  • Blazor 使用序列号,而其他树上差分 UI 框架不使用它们。 使用序列号时,差分速度要快得多,并且 Blazor 的优势在于编译步骤可为编写 .razor 文件的开发人员自动处理序列号。