生命周期
一、组件生命周期
组件从创建到销毁的过程中,会触发一系列的内置事件,而订阅这些事件的内置方法,称为Razor组件的生命周期方法。
我们在定义组件时,可以通过重写这些生命周期方法以在组件初始化和渲染期间对组件执行其他操作。
如上图所示,组件的生命周期事件顺序如下:
1.创建组件实例(组件第一次渲染时发生)
2.调用SetParametersAsync
,以执行属性的注入(组件第一次渲染时发生)
3.调用OnInitialized
、OnInitializedAsync
方法(组件第一次渲染时发生)
OnInitialized
执行完成后,如果OnInitializedAsync
方法未完成,则先进行一次渲染,并等待OnInitializedAsync
完成后,再重新渲染。
4.OnInitialized
和OnInitializedAsync
完成后,调用OnParametersSet
、OnParametersSetAsync
方法。
OnParametersSet
执行完成后,如果OnParametersSetAsync
方法未完成,则先进行一次渲染,并等待OnParametersSetAsync
完成后重新渲染组件。
5.在渲染完成后调用OnAfterRender
、OnAfterRenderAsync
。
示例
@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>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
protected override void OnInitialized()
{
currentCount = 1;
}
protected override async Task OnInitializedAsync()
{
await Task.Delay(2000);
currentCount = 2;
}
protected override async Task OnParametersSetAsync()
{
await Task.Delay(5000);
currentCount = 4;
}
protected override void OnParametersSet()
{
currentCount = 3;
}
}
注意,由于要决定需要渲染哪些子组件,因此父组件的渲染是要先于子组件的。
1、SetParametersAsync
Task SetParametersAsync(ParameterView parameters)
:用于设置组件中的使用了[Parameter]
或 [CascadingParameter]
特性标注的属性值。
- 默认情况下,
SetParametersAsync
方法会自动完成属性值的填充,如果我们在这个过程需要进行业务的处理,可以重写这个方法,并在处理完成后调用base.SetParametersAsync
,当然了,如果不需要进行自动的数据填充,不调也是可以的。 parameters
:表示参数视图,存放了来自父组件或路由传入的所有参数值。- 如果组件为交互式渲染且支持预渲染,那么会执行两次
TryGetValue<T>(string parameterName, out T result)
:ParameterView
的实例方法,从参数视图中获取指定的参数,如果有则返回true
并把值设置到result
中,否则返回false
并将result
设置为null
。
- 注意,虽然路由模板上面的参数是不区分大小写的,但是使用这个方法去路由模板中获取参数值的时候是要区分的。
示例-可路由组件传参
@page "/setParameters/{Name?}"
<PageTitle>Set Parameters Async</PageTitle>
<h1>Set Parameters Async Example</h1>
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string? Name { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>("Name", out var value))
{
if (value is null)
{
message = "The value of 'Name' is null.";
}
else
{
message = $"The value of 'Name' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
示例-组件实例传参
<PageTitle>Set Parameters Async</PageTitle>
<h1>Set Parameters Async Example</h1>
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string? Name { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>("Name", out var value))
{
if (value is null)
{
message = "The value of 'Name' is null.";
}
else
{
message = $"The value of 'Name' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
@page "/paramTest"
<SetParamsAsync Name="Schuyler"/>
注意,不论是路由传参还是对组件实例传参,都需要在组件内部先定义对应的属性并且使用[Parameter]
标注。
2、OnInitialized
OnInitialized()
/OnInitializedAsync()
:组件在接收SetParametersAsync
中的初始参数后会进行组件的初始化,此时将调用这两个函数。
- 同步初始化方法,确保了父组件在子组件之前完成了初始化,而异步初始化方法则无法确定两者的初始化完成顺序。
- 如果组件为交互式渲染且支持预渲染,那么会执行两次
示例-同步
@page "/on-init"
<PageTitle>On Initialized</PageTitle>
<h1>On Initialized Example</h1>
<p>@message</p>
@code {
private string? message;
protected override void OnInitialized()
{
message = $"Initialized at {DateTime.Now}";
}
}
示例-异步
若要执行异步操作,重写 OnInitializedAsync
并使用 await
运算符
protected override async Task OnInitializedAsync()
{
await ...
}
3、OnParametersSet
OnParametersSet()
/OnParametersSetAsync()
:这两个方法是在参数完成设置后调用的,具体会在以几种情况下调用。
- 在
OnInitialized
或OnInitializedAsync
方法都完成之后调用。 - 当父组件重新渲染并提供以下内容时调用
- 至少有一个参数发生更改时。需要注意的是,如果参数是引用类型,由于框架无法知道内部是否发生改变,因此如果存在一个或多个引用类型参数那么框架始终会认为发生了参数的更改。
- 如果组件为交互式渲染且支持预渲染,那么会执行两次
在组件参数完成设置后,需要对参数进行一些相关处理的时候,可以重写OnParametersSet()
或OnParametersSetAsync()
。
例如,在组件路由中,无法对具有datetime
约束的参数进行约束,也无法使其称为可选参数,此时就需要在组件中设置两个路由,并通过OnParametersSet()
方法来进行处理。
示例
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"
<PageTitle>On Parameters Set</PageTitle>
<h1>On Parameters Set Example</h1>
<p>
Pass a datetime in the URI of the browser's address bar.
For example, add /1-1-2024 to the address.
</p>
<p>@message</p>
@code {
private string? message;
[Parameter]
public DateTime StartDate { get; set; }
protected override void OnParametersSet()
{
if (StartDate == default)
{
StartDate = DateTime.Now;
message = $"No start date in URL. Default value applied " +
$"(StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used " +
$"(StartDate: {StartDate}).";
}
}
}
4、OnAfterRender
OnAfterRender(bool firstRender)
/OnAfterRenderAsync(bool firstRender)
:在组件完成渲染后调用。
firstRender
:在第一次渲染组件实例时会设置为true
,用来确保初始化操作仅执行一次。- 这两个方法在组件在预渲染时候不调用。
- 对于
OnAfterRenderAsync(bool firstRender)
方法,在完成任务并返回Task后,并不会进行渲染,这点与上面几个生命周期方法都有点不同,上面几个生命周期方法,在完成后框架都会安排进行渲染,而OnAfterRenderAsync
方法不会,这是为了避免无限渲染循环。
在这两个方法中,可使用渲染的内容执行其他初始化步骤,例如调用与渲染的DOM元素进行交互的JS操作。
示例
@page "/after-render"
@inject ILogger<AfterRender> Logger
<PageTitle>After Render</PageTitle>
<h1>After Render Example</h1>
<p>
<button @onclick="LogInformation">Log information (and trigger a render)</button>
</p>
<p>Study logged messages in the console.</p>
@code {
private string message = "Initial assigned message.";
protected override void OnAfterRender(bool firstRender)
{
Logger.LogInformation("OnAfterRender(1): firstRender: " +
"{FirstRender}, message: {Message}", firstRender, message);
if (firstRender)
{
message = "Executed for the first render.";
}
else
{
message = "Executed after the first render.";
}
Logger.LogInformation("OnAfterRender(2): firstRender: " +
"{FirstRender}, message: {Message}", firstRender, message);
}
private void LogInformation()
{
Logger.LogInformation("LogInformation called");
}
}
此外需要注意的是,渲染后立即进行的异步操作必须在OnAfterRenderAsync
方法中进行。
二、异步操作未完成时的渲染处理
上面生命周期方法中出现了多个异步方法,当我们在组件中重写这些异步方法并对组件中所使用的某个变量对象进行定义时,需要注意组件渲染时,变量为null
的情况,可以做如下处理。
示例
<h1>Sci-Fi Movie Ratings</h1>
@if (movies == null)
{
<p><em>Loading...</em></p>
}
else
{
<ul>
@foreach (var movie in movies)
{
<li>@movie.Title — @movie.Rating</li>
}
</ul>
}
@code {
private Movies[]? movies;
protected override async Task OnInitializedAsync()
{
movies = await GetMovieRatings(DateTime.Now);
}
}
组件资源的释放
组件上有时会存在一些需要进行释放的资源,尤其在与JS进行交互操作时,会产生一些非托管资源,如果不进行释放,可能会造成内存泄漏。
如果希望从UI中删除组件时,框架会调用自动进行组件的资源释放,就需要让组件实现 IDisposable
或 IAsyncDisposable
接口。
- 注意两者效果上是一样的,不需要同时实现,如果两者同时实现,框架只会执行异步重载。
一、同步IDisposable
要实现同步资源释放,可以使用IDisposable
的Dispose
方法。
实现方式
- 组件中使用
@implements
指令实现IDisposable
接口。 - 实现
Dispose
方法,并释放资源。 - 如果要释放的资源是在生命周期方法中创建的,需要执行
null
检查。
示例
@page "/timer-disposal-2"
@using System.Timers
@implements IDisposable
<PageTitle>Timer Disposal 2</PageTitle>
<h1>Timer Disposal Example 2</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer? timer;
protected override void OnInitialized()
{
timer = new Timer(1000);
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer?.Dispose();
}
二、异步 IAsyncDisposable
要实现异步资源释放,可以使用IAsyncDisposable
的DisposeAsync
方法。
实现方式
- 组件中使用
@implements
指令实现IAsyncDisposable
接口。 - 实现
IAsyncDisposable
方法,并释放资源。 - 如果要释放的资源是在生命周期方法中创建的,需要执行
null
检查。
示例
@implements IAsyncDisposable
...
@code {
...
public async ValueTask DisposeAsync()
{
if (obj is not null)
{
await obj.DisposeAsync();
}
}
}
注意,不支持在 Dispose
/DisposeAsync
中调用StateHasChanged
。 StateHasChanged
可能在拆除渲染器时调用,因此不支持在此时请求 UI 更新。
三、null分配到已释放对象
在释放资源之后,是否需要将对象设置为null
,这个问题,我自己在写代码的时候是习惯性会给他设为null
的,但是官方给出了不一样的说法。
通常,在调用Dispose
/DisposeAsync
后无需将 null
分配到已释放的对象。 分配 null
的罕见情况包括:
- 防止重复调用
Dispose
/DisposeAsync
- 如果一个长时间运行的进程继续引用已释放的对象,则分配
null
将允许垃圾回收器释放该对象,即使长时间运行的进程持续引用它也是如此。
对于正确实现并正常运行的对象,没有必要将 null
分配给已释放的对象。 在必须为对象分配 null
的罕见情况下,建议记录原因,并寻求一个防止需要分配 null
的解决方案。
四、取消订阅
PS:这个使用之后发现,必须要这么处理,不然离开组件后,组件对象并不能进行垃圾回收,而是一直存在。
如果在组件中进行了 .NET 事件的订阅,那么在组件消亡时应该取消订阅,结合上面的Dispose
的用法,就可以在Dispose
中完成取消订阅的操作。
- 取消事件的订阅后,才能允许组件进行垃圾回收。
示例
@implements IDisposable
<EditForm EditContext="@editContext">
...
<button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>
@code {
...
protected override void OnInitialized()
{
editContext = new(model);
editContext.OnFieldChanged += HandleFieldChanged;
}
private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
{
...
}
public void Dispose()
{
editContext.OnFieldChanged -= HandleFieldChanged;
}
}
需要注意的是,如果是直接使用lambda表达式进行事件订阅的话,无需显式释放。
- 虽然官方这么说,但是我自己试了一下这种方式,组件是无法完成垃圾回收的,还是建议使用具名方法,然后取消订阅。
//如果是直接这样订阅的话,没必要再手动做取消订阅
editContext.OnFieldChanged += (sender,e) => {};