Blazor 组件何时进行渲染?

7
在下面从此页面中的图表中,显示了当通过调用OnInitializedAsync返回一个未完成的任务时,它将等待该任务,然后呈现组件。
然而,实际发生的情况似乎是在返回未完成的任务时立即呈现组件,然后再次呈现一次一旦未完成的任务完成。 enter image description here 页面后面的示例似乎证实了这一点。如果在调用OnInitializedAsync后没有立即呈现组件,而是仅在返回的任务完成后首次呈现,您将永远看不到"Loading..."消息。
OnParametersSetAsync的行为似乎也是相同的。当返回一个未完成的任务时,它会立即呈现一次,然后再次呈现一次该任务完成之后。
我是否误解了呈现生命周期,还是文档中有错误?
谢谢
@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <!-- forecast data in table element content -->
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

1
您的组件被渲染了两次。第一次是在等待ForecastService.GetForecastAsync时。第二次是在ForecastService.GetForecastAsync返回时。当等待ForecastService.GetForecastAsync时,控制权会移交给调用代码,并首次呈现控件以显示消息“Loading...”。当ForecastService.GetForecastAsync返回时,方法StateHasChanged将被隐式调用,以重新呈现组件进行第二次渲染。请参见:https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ComponentBase.cs - enet
2个回答

7

简短摘要

  • 从概念上讲,Blazor在每个生命周期事件和UI事件之前和之后都添加了两个“免费”的StateHasChanged调用。
  • StateHasChanged只是请求更新html,而不执行更新。
  • 只有在事件发生后或主线程被await释放时,才能满足更新请求。
    • 并非每个await都会释放线程。

因此,当您希望确保屏幕得到更新时,请使用

StateHasChanged();
await Task.Delay(1);  // Task.Yield() does not always work

旧答案

当一个不完整的任务被返回时,它会立即渲染组件,然后在不完整的任务完成后再次渲染。

是的,这是一种可能的顺序。

流程图显示了显示组件的步骤。从图片上并不太清楚的是,实际的渲染不是这个流程的一部分,它是异步发生在同步上下文中的。它可以在你的代码中使用await时发生。

所以我们有这个基本的非异步序列:

  • Oninitialzed[Async]

  • OnParametersSet[Async]

  • Render

  • OnAfterRender[Async]

但是当这段代码路径中有异步内容时,在await期间可能会有额外的渲染。在此流程中调用StateHasChanged时可能会出现更多的渲染。


Blazor WebAssembly 是单线程的,它在同步上下文中异步执行。 - enet
在我看来,你似乎没有看到异步编程和多线程之间的区别。 - enet
1
@HenkHolterman - 【非常客气地说】很难措辞得恰当。"released by an await"可能会误导人。我知道你后来有进行限定说明。你可以在一个Task中包装同步代码块并使用await。重要的是"yielding"。 - MrC aka Shaun Curtis
@MrCakaShaunCurtis 是的,但“yielding”与“releasing the thread”是同义词,我在那里看不到矛盾。但你是对的,你可能会错误地读取第一部分的await。这种情况经常发生。我保持原样,我认为“not every await ...”部分更强大。 - H H
@enet:是的,其中一个完全没有头绪。 - H H
显示剩余2条评论

4
为了完整回答你的问题,我们需要深入了解ComponentBase代码。
你的代码运行在异步世界中,其中代码块可以yield并将控制权返回给调用者 - 你的“不完整任务被返回”。
当组件首次渲染时以及任何参数发生更改时,渲染器会调用SetParametersAsync
public virtual Task SetParametersAsync(ParameterView parameters)
{
    parameters.SetParameterProperties(this);
    if (!_initialized)
    {
        _initialized = true;
        return RunInitAndSetParametersAsync();
    }
    else
        return CallOnParametersSetAsync();
}

RunInitAndSetParametersAsync负责初始化。我保留了微软编码人员的注释,其中解释了StateHasChanged的调用。

private async Task RunInitAndSetParametersAsync()
{
    OnInitialized();
    var task = OnInitializedAsync();

    if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
    {
        // Call state has changed here so that we render after the sync part of OnInitAsync has run
        // and wait for it to finish before we continue. If no async work has been done yet, we want
        // to defer calling StateHasChanged up until the first bit of async code happens or until
        // the end. Additionally, we want to avoid calling StateHasChanged if no
        // async work is to be performed.
        StateHasChanged();
        try
        {
            await task;
        }
        catch // avoiding exception filters for AOT runtime support
        {
            if (!task.IsCanceled)
                throw;
        }
        // Don't call StateHasChanged here. CallOnParametersSetAsync should handle that for us.
    }
    await CallOnParametersSetAsync();
}

CallOnParametersSetAsync会在每次参数更改时被调用。

private Task CallOnParametersSetAsync()
{
    OnParametersSet();
    var task = OnParametersSetAsync();
    // If no async work is to be performed, i.e. the task has already ran to completion
    // or was canceled by the time we got to inspect it, avoid going async and re-invoking
    // StateHasChanged at the culmination of the async work.
    var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
        task.Status != TaskStatus.Canceled;

    // We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
    // the synchronous part of OnParametersSetAsync has run.
    StateHasChanged();

    return shouldAwaitTask ?
        CallStateHasChangedOnAsyncCompletion(task) :
        Task.CompletedTask;
}

在上面的代码中,将“StateHasChanged”替换为“Render”。图表使用了“Render”的词汇,这有点误导人。它暗示了UI重新渲染,而实际发生的是一个渲染片段(构建组件UI标记的代码块)被排队到Renderer的渲染队列中。应该说“请求渲染”或类似的话语。如果触发渲染事件的组件代码或调用“StateHasChanged”的代码都是同步代码,则Renderer仅在代码完成时获得线程时间。在此过程中,代码块需要“放弃”以使Renderer获得线程时间。还要注意,不是所有基于任务的方法都会产生“放弃”。许多只是包装在任务中的同步代码。因此,如果在OnInitializedAsyncOnParametersSetAsync中的代码中产生“放弃”,则首次放弃时会有一个渲染事件,然后完成时会再次进行渲染。在同步代码块中“放弃”的常见做法是在想要Renderer渲染的位置添加此行代码。
await Task.Delay(1);

您可以在这里看到 ComponentBase - https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ComponentBase.cs


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接