第三章 组件(2)- 生命周期与资源释放

生命周期

一、组件生命周期

组件从创建到销毁的过程中,会触发一系列的内置事件,而订阅这些事件的内置方法,称为Razor组件的生命周期方法。
我们在定义组件时,可以通过重写这些生命周期方法以在组件初始化和渲染期间对组件执行其他操作。
在这里插入图片描述
如上图所示,组件的生命周期事件顺序如下:
1.创建组件实例(组件第一次渲染时发生)
2.调用SetParametersAsync,以执行属性的注入(组件第一次渲染时发生)
3.调用OnInitializedOnInitializedAsync方法(组件第一次渲染时发生)

  • OnInitialized执行完成后,如果OnInitializedAsync方法未完成,则先进行一次渲染,并等待OnInitializedAsync完成后,再重新渲染。

4.OnInitializedOnInitializedAsync完成后,调用OnParametersSetOnParametersSetAsync方法。

  • OnParametersSet执行完成后,如果OnParametersSetAsync方法未完成,则先进行一次渲染,并等待OnParametersSetAsync完成后重新渲染组件。

5.在渲染完成后调用OnAfterRenderOnAfterRenderAsync

示例

@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():这两个方法是在参数完成设置后调用的,具体会在以几种情况下调用。

  • OnInitializedOnInitializedAsync 方法都完成之后调用。
  • 当父组件重新渲染并提供以下内容时调用
    • 至少有一个参数发生更改时。需要注意的是,如果参数是引用类型,由于框架无法知道内部是否发生改变,因此如果存在一个或多个引用类型参数那么框架始终会认为发生了参数的更改。
  • 如果组件为交互式渲染且支持预渲染,那么会执行两次

在组件参数完成设置后,需要对参数进行一些相关处理的时候,可以重写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 &mdash; @movie.Rating</li>
        }
    </ul>
}

@code {
    private Movies[]? movies;

    protected override async Task OnInitializedAsync()
    {
        movies = await GetMovieRatings(DateTime.Now);
    }
}

组件资源的释放

组件上有时会存在一些需要进行释放的资源,尤其在与JS进行交互操作时,会产生一些非托管资源,如果不进行释放,可能会造成内存泄漏。
如果希望从UI中删除组件时,框架会调用自动进行组件的资源释放,就需要让组件实现 IDisposableIAsyncDisposable接口。

  • 注意两者效果上是一样的,不需要同时实现,如果两者同时实现,框架只会执行异步重载。

一、同步IDisposable

要实现同步资源释放,可以使用IDisposableDispose方法。

实现方式

  • 组件中使用@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

要实现异步资源释放,可以使用IAsyncDisposableDisposeAsync方法。

实现方式

  • 组件中使用@implements指令实现IAsyncDisposable接口。
  • 实现IAsyncDisposable方法,并释放资源。
  • 如果要释放的资源是在生命周期方法中创建的,需要执行null检查。

示例

@implements IAsyncDisposable

...

@code {
    ...

    public async ValueTask DisposeAsync()
    {
        if (obj is not null)
        {
            await obj.DisposeAsync();
        }
    }
}

注意,不支持在 Dispose/DisposeAsync 中调用StateHasChangedStateHasChanged可能在拆除渲染器时调用,因此不支持在此时请求 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) => {};
  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SchuylerEX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值