组件的渲染
一、组件的渲染规则
当组件第一次通过父组件添加到组件层次结构时,它们必须渲染。而在其他情况下,组件可以按照各自的逻辑和规则渲染。
默认情况下,Razor
组件继承自ComponentBase
基类,该基类包含在以下节点触发重新渲染的逻辑:
- 从父组件使用一组已更新的参数之后
- 为级联参数使用已更新的值之后
- 触发事件并调用其自己的某个事件处理程序(例如触发
button
控件的@onclick
事件)之后 - 在组件调用自己的
StateHasChanged
方法后
二、控制渲染
组件的每一次渲染都会先调用ShouldRender()
方法,根据其返回的结果来决定要不要继续进行渲染,因此,我们如果想要管理UI的渲染与否,可以通过重写ShouldRender()
方法来实现。
需要注意的是,不管ShouldRender()
返回啥结果,组件始终会完成最初的渲染。
示例-counter
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<SetParamsAsync Name="Schuyler" />
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
protected override bool ShouldRender()
{
return false;
}
}
例子中在counter组件中重写ShouldRender()
并直接返回false,可以看到,组件虽然完成了最初的渲染,但后面无论怎么点击按钮,组件都不会发生改变。
三、状态更改通知
StateHasChanged()
:通知组件有状态发生了改变,需要立刻进行渲染。
通过调用 StateHasChanged
,可以随时触发渲染,但是注意避免不必要的调用以产生不必要的渲染成本。
对于以下几种情况,是不需要调用StateHasChanged
的:
- 定期处理时间中,因为
ComponentBase
会触发大多数常规事件处理程序的渲染。 - 在重写
OnInitialized
、OnInitializedAsync
、OnParametersSet
、OnParametersSetAsync
等典型生命周期方法时,不需要调用StateHasChanged
,因为这些方法在完成后都会触发渲染。
下面几种情况,可能适合调用StateHasChanged
:
异步业务处理过程中,存在多个不同的异步阶段
示例
@page "/counter-state-1"
<PageTitle>Counter State 1</PageTitle>
<h1>Counter State Example 1</h1>
<p>
Current count: @currentCount
</p>
<p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>
@code {
private int currentCount = 0;
private async Task IncrementCount()
{
currentCount++;
// Renders here automatically
await Task.Delay(1000);
currentCount++;
StateHasChanged();
await Task.Delay(1000);
currentCount++;
StateHasChanged();
await Task.Delay(1000);
currentCount++;
// Renders here automatically
}
}
从Blazor渲染和事件处理系统外部接收调用,ComponentBase
只知道其自身的生命周期方法和 Blazor 触发的事件(例如@onclick
啥的),对于其他的节点所发生的事情并不清楚,如果希望在此类事件中触发渲染,就可以调用StateHasChanged
示例
@page "/counter-state-2"
@using System.Timers
@implements IDisposable
<PageTitle>Counter State 2</PageTitle>
<h1>Counter State Example 2</h1>
<p>
This counter demonstrates <code>Timer</code> disposal.
</p>
<p>
Current count: @currentCount
</p>
@code {
private int currentCount = 0;
private Timer timer = new(1000);
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
}
父组件的刷新规则
如果在父组件中调用StateHasChanged()
,Blazor框架会根据如下规则去重新渲染子组件:
- 父组件中使用子组件时,如果给子组件传递了组件参数,当调用
StateHasChanged()
时,传递的组件参数发生了变化,则会重新渲染子组件。 - 如果组件参数是引用类型,由于框架无法知道内部是否发生改变,因此如果存在一个或多个引用类型组件参数那么框架始终会认为发生了组件参数的更改。子内容
RenderFragment
类型也属于此列,无论子内容有没有更改,都后刷新子组件。
例如下面的示例中,在父组件中调用StateHasChanged()
时:
- 第一个ShowMoreExpander组件设置了子内容,因此在父组件中调用
StateHasChanged()
后会自动重新渲染该组件,并且会将InitiallyExpanded的值覆盖为其初始值false
。 - 第二个ShowMoreExpander组件只设置了组件参数,且父组件中设置的组件参数值没有发生变化,因此在父组件中调用
StateHasChanged()
后不会重新渲染该组件。
ShowMoreExpander.razor
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
<div class="card-header">
<h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
</div>
@if (InitiallyExpanded)
{
<div class="card-body">
<p class="card-text">@ChildContent</p>
</div>
}
</div>
@code {
[Parameter]
public bool InitiallyExpanded { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
private void ShowMore()
{
InitiallyExpanded = true;
}
}
Expanders.razor
@page "/expanders"
<PageTitle>Expanders</PageTitle>
<h1>Expanders Example</h1>
<ShowMoreExpander InitiallyExpanded=false>
Expander 1 content
</ShowMoreExpander>
<ShowMoreExpander InitiallyExpanded=false/>
<button @onclick="StateHasChanged">Call StateHasChanged</button>
四、流式渲染
将流式渲染与交互式服务器端渲染(SSR) 一起使用,以在响应流上流式传输内容更新,并改善执行长期运行的异步任务来实现完整渲染的用户体验。
假设在页面加载时,有一个组件需要执行长时间的数据库查询或 Web API 调用来渲染数据。 通常,作为渲染服务器端组件其中一部分来执行的异步任务必须在发送响应之前完成,这会导致页面加载延迟。 渲染页面时出现严重延迟会破坏用户体验。 为改善用户体验,流式渲染最开始使用占位符内容快速渲染整个页面,同时执行异步操作。 操作完成后,更新的内容将在同一响应连接上发送到客户端,并修补到文档对象模型中。
默认情况下,交互式渲染模式(Server或WebAssembly)的组件会自动采用流式渲染,如果是在静态服务器端渲染的组件中想要使用流式渲染,那么需要通过@attribute
指定为该组件添加[StreamRendering(true)]
特性。
示例
@page "/weather"
@attribute [StreamRendering(true)]
...
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
...
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
...
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
await Task.Delay(500);
...
forecasts = ...
}
}
如果父组件使用此功能,则没有该属性的组件会自动采用流式渲染。也可以将 false
传递给子组件中的属性以禁用此功能。
五、组件渲染模式的传播
渲染模式会向下传播,其规则如下:
- 默认的渲染模式为静态
- 交互式服务器 (
InteractiveServer
)、交互式 WebAssembly (InteractiveWebAssembly
) 和交互式自动 (InteractiveAuto
) 渲染模式对组件实例使用,包括对同级组件使用不同的渲染模式 - 无法在子组件中切换到其他交互式渲染模式。 例如,Server组件不能是 WebAssembly 组件的子组件(静态是可以的)
- 从静态父级传递到交互式子组件的参数必须是 JSON 可序列化的。 这意味着无法将渲染片段或子内容从静态父级组件传递到交互式子组件
以SharedMessage组件为例子,SharedMessage组件定义时使用默认的渲染模式,在几种不同情况下的表现。
SharedMessage.razor
<p>@Greeting</p>
<button @onclick="UpdateMessage">Click me</button> @message
<p>@ChildContent</p>
@code {
private string message = "Not clicked yet.";
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string Greeting { get; set; } = "Hello!";
private void UpdateMessage()
{
message = "Somebody clicked me!";
}
}
继承父类渲染模式
将 SharedMessage 组件在指定了渲染模式的组件中使用时,SharedMessage组件将会继承所在组件的渲染模式。
@page "/render-mode-1"
@rendermode InteractiveServer
//此时,SharedMessage使用交互式服务端渲染模式
<SharedMessage />
对组件实例使用不同的渲染模式
在组件中使用SharedMessage组件时,可以单独对SharedMessage组件的实例使用不同的渲染模式
注意,如果父组件指定了InteractiveServer
模式,则子组件不能使用InteractiveWebAssembly
模式,反之亦然。
@page "/render-mode-2"
<SharedMessage @rendermode="InteractiveServer" />
<SharedMessage @rendermode="InteractiveWebAssembly" />
六、保留预渲染状态
默认情况下,交互式渲染的组件是支持预渲染的,这种情况下,组件在进行初始化加载时,如果预渲染期间设置了状态值,那么当交互式渲染完成后这个状态值将会丢失。
例如,某个组件在生命周期OnInitialized
方法中,对某个变量进行了随机赋值,在预渲染时会调用OnInitialized
方法,此时变量会受到第一次赋值,当客户端建立了SignalR连接后,组件进行交互式渲染,会再次调用OnInitialized
方法,此时变量再次被赋予随机值,两次的值明显是不一样的。
示例
@page "/counterTets"
@rendermode @(new InteractiveServerRenderMode(prerender: true))
@inject ILogger<CounterTest> Logger
<PageTitle>counterTets</PageTitle>
<h1>counterTets</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount;
protected override void OnInitialized()
{
currentCount = Random.Shared.Next(100);
Logger.LogInformation("currentCount set to {Count}", currentCount);
}
private void IncrementCount()
{
currentCount++;
}
}
如果想要保留在预渲染期间产生的值,需要使用PersistentComponentState
服务,其常规写法如下:
示例
@page "/counterTets"
@rendermode InteractiveServer
@inject PersistentComponentState ApplicationState
<PageTitle>counterTets</PageTitle>
<h1>counterTets</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount;
private PersistingComponentStateSubscription persistingSubscription;
private void IncrementCount()
{
currentCount++;
}
protected override void OnInitialized()
{
persistingSubscription =
ApplicationState.RegisterOnPersisting(PersistData);
if (!ApplicationState.TryTakeFromJson<int> ( nameof(currentCount), out var restored))
{
currentCount = Random.Shared.Next(100);
}
else
{
currentCount = restored!;
}
}
private Task PersistData()
{
ApplicationState.PersistAsJson(nameof(currentCount), currentCount);
return Task.CompletedTask;
}
void Dispose()
{
persistingSubscription.Dispose();
}
}