Жизненный цикл компонента Blazor: полное руководство

· 4 мин чтения

Я использую Blazor уже некоторое время, и, честно говоря, методы жизненного цикла поначалу меня смутили. OnInitialized против OnInitializedAsync? OnParametersSet против OnAfterRender? Когда StateHasChanged фактически запускает повторный рендеринг? После многих проб и ошибок у меня наконец появилась твердая мысленная модель всего этого.

Краткий обзор жизненного цикла

При рендеринге компонента Blazor он проходит через следующие методы по порядку:

  1. [[[ТОК_5]]]
  2. [[[ТОК_6]]] / [[[ТОК_7]]]
  3. [[[ТОК_8]]] / [[[ТОК_9]]]
  4. [[[ТОК_10]]] / [[[ТОК_11]]]
  5. [[[ТОК_12]]] / [[[ТОК_13]]]

Давайте пройдемся по каждому.

УстановитьПараметрыАсинхронно

Это самый первый вызываемый метод. Он получает необработанный ParameterView от родительского компонента. В большинстве случаев вы не переопределяете это — Blazor автоматически обрабатывает параметры сопоставления. Но если вам нужна обработка или проверка пользовательских параметров перед их назначением:

public override async Task SetParametersAsync(ParameterView parameters)
{
    // Custom logic before parameters are set
    if (parameters.TryGetValue<string>("Title", out var title))
    {
        Console.WriteLine($"Title is being set to: {title}");
    }

    await base.SetParametersAsync(parameters);
}

Я использовал это ровно один раз в реальном проекте. Большую часть времени вы пропустите это.

OnInitialized/OnInitializedAsync

Здесь вы выполняете работу по настройке — загружаете данные, инициализируете службы, устанавливаете значения по умолчанию. Он запускается один раз при первом создании компонента.

@code {
    private List<Product> products = new();

    protected override async Task OnInitializedAsync()
    {
        products = await Http.GetFromJsonAsync<List<Product>>("api/products");
    }
}

Одна вещь, которая меня смутила: на Blazor Server OnInitializedAsync вызывается дважды во время предварительного рендеринга. Первый раз во время предварительной отрисовки на стороне сервера, а второй раз — при установке соединения SignalR. Если ваш вызов API является дорогостоящим, вы можете решить эту проблему:

private bool isPrerendering = true;

protected override async Task OnInitializedAsync()
{
    products = await Http.GetFromJsonAsync<List<Product>>("api/products");
    isPrerendering = false;
}

Или, что еще лучше, используйте PersistentComponentState, чтобы полностью избежать двойного вызова.

OnParametersSet / OnParametersSetAsync

Это срабатывает каждый раз, когда родительский компонент выполняет повторную визуализацию и передает новые значения параметров. Он также срабатывает после OnInitialized. Это подходящее место для реагирования на изменения параметров:

[Parameter]
public int CategoryId { get; set; }

private int previousCategoryId;
private List<Product> products = new();

protected override async Task OnParametersSetAsync()
{
    if (CategoryId != previousCategoryId)
    {
        previousCategoryId = CategoryId;
        products = await Http.GetFromJsonAsync<List<Product>>(
            $"api/products?category={CategoryId}");
    }
}

Проверка на CategoryId != previousCategoryId важна — без нее вы бы перезагружали данные каждый раз, когда родительский элемент выполняет повторную визуализацию, даже если категория не изменилась.

OnAfterRender / OnAfterRenderAsync

Это срабатывает после того, как компонент отображается в DOM. Параметр firstRender сообщает вам, является ли это первоначальным рендерингом. Это место для вызовов JS Interop, поскольку на этом этапе существуют элементы DOM:

[Inject]
private IJSRuntime JS { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await JS.InvokeVoidAsync("initializeChart", chartElement);
    }
}

Я часто использую firstRender, чтобы избежать повторной инициализации библиотек JavaScript при каждом повторном рендеринге. Если вы настраиваете прослушиватели событий, библиотеки диаграмм или что-то, что напрямую касается DOM, это то, что вам нужно.

ДолженRender

Этот менее известен, но очень полезен. Он контролирует, будет ли компонент выполнять повторную визуализацию при вызове StateHasChanged:

private bool shouldRender = true;

protected override bool ShouldRender() => shouldRender;

private void HeavyOperation()
{
    shouldRender = false;

    // Do a bunch of state changes without triggering renders
    for (int i = 0; i < 1000; i++)
    {
        items[i].Process();
    }

    shouldRender = true;
    StateHasChanged(); // Now render once with all changes
}

Я использовал это для оптимизации компонентов, обрабатывающих большие списки. Вместо повторного рендеринга при каждом изменении элемента вы выполняете пакетную обработку обновлений и выполняете рендеринг один раз в конце.

Dispose/DisposeAsync

Когда компонент удаляется из пользовательского интерфейса, вам следует очистить все ресурсы. Реализуйте IDisposable или IAsyncDisposable:

@implements IAsyncDisposable

@code {
    private Timer? timer;
    private IJSObjectReference? jsModule;

    protected override void OnInitialized()
    {
        timer = new Timer(OnTick, null, 0, 1000);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "./timer.js");
        }
    }

    public async ValueTask DisposeAsync()
    {
        timer?.Dispose();

        if (jsModule is not null)
        {
            await jsModule.DisposeAsync();
        }
    }
}

Общие вещи, которые нужно удалить: таймеры, подписки на события, ссылки на модули JS, CancellationTokenSource и любые созданные вами службы IDisposable.

СостояниеИзмененоЭто не метод жизненного цикла, но он тесно связан. Он сообщает Blazor: «Эй, мое состояние изменилось, пожалуйста, перерисуйте меня». Blazor вызывает его автоматически после обработчиков событий, но иногда вам нужно вызвать его вручную — обычно, когда состояние изменяется вне обычного потока событий Blazor:

private async Task StartPolling()
{
    while (!cts.Token.IsCancellationRequested)
    {
        data = await Http.GetFromJsonAsync<Data>("api/data");
        StateHasChanged(); // Manual call needed since this isn't a Blazor event
        await Task.Delay(5000, cts.Token);
    }
}

Одно важное замечание: если вы обновляетесь из фонового потока на Blazor Server, используйте InvokeAsync:

await InvokeAsync(() =>
{
    data = newData;
    StateHasChanged();
});

Полная картина

Вот порядок, в котором все происходит:

Component created
  └─ SetParametersAsync
       └─ OnInitialized / OnInitializedAsync
            └─ OnParametersSet / OnParametersSetAsync
                 └─ Render
                      └─ OnAfterRender(firstRender: true)

Parameter change from parent
  └─ SetParametersAsync
       └─ OnParametersSet / OnParametersSetAsync
            └─ ShouldRender?
                 └─ Render
                      └─ OnAfterRender(firstRender: false)

Component removed
  └─ Dispose / DisposeAsync

Если у вас в голове этот поток, отладка проблем жизненного цикла становится намного проще.

Надеюсь, вам понравился пост! Не стесняйтесь обращаться ко мне в любой социальной сети по адресу @emimontesdeoca.

Ресурсы