属性如何导致页面在 Blazor 中更新?

How do properties cause pages to update in Blazor?

我刚刚在 Blazor WebAssembly 中完成了我的第一个重要测试应用程序。 Blazor 是令人印象深刻的东西,但我发现很难推断属性更改如何导致更新 DOM - 例如,在 Razor 组件中引用 属性 的地方。

<div>@SomeProperty</div>
public int SomeProperty {get;set;}

在 WPF 中,很容易推断出更改是如何流动的并导致呈现更改,因为它们是由事件和 DependencyProperty 更改触发的。您可以看到这些并绑定到它们。在 Blazor 中,您可以通过某种方式更改 属性 值并更新页面。这背后的精确机制有点像魔法。因此,很难推断如何删除复杂组件的不必要更新。

谁能解释一下这个话题的基础?

是否有任何深入介绍 Blazor 领域的文档或视频?

In Blazor, somehow you can change a property value and the page updates.

不完全是。 属性 本身与此无关。例如,您可以 并且您不会看到 UI 发生变化。

但通常您会设置 属性 以响应 ButtonClick 或其他 Blazor(生命周期)事件。还有那些 are bracketed with calls to StateHasChanged()。在计时器事件或其他 non-Blazor 事件中,您必须自己调用 StateHasChanged()。

Blazor 维护自己的 DOM 副本(快速)。在 StateHasChanged() 之后,该副本被重建并与前一个副本进行比较。任何更改都会应用到实际的 DOM 和 JavaScript。

As a result, it can be hard to reason about how to remove unnecessary updates for complex components.

这与 <div>@SomeProperty</div> 不同。

当您有 <Details ItemId="itemId" />itemId 是基本类型(如 int 或字符串)时,当 itemId 更改时,详细信息将仅 re-rendered。

但是当它是 <Details Item="item" /> 并且项目是某种复杂类型时,组件将始终与父页面一起重新呈现。

我有时在 Details 组件中使用以下模式,当 Item 没有可变属性时,即 <Details Item="selectedItem" />:

[Parameter]
public Item Item { get; set; } = new();

int oldId = 0;

protected override bool ShouldRender()
{
    if (oldId != Item.Id)
    {
        oldId = Item.Id;
        return true;
    }
    return false;
}

Can anyone explain the underpinnings of this topic?

要了解更新过程,您需要了解组件。我会尽量保持简短!

所有组件都必须实现 IComponentComponentBaseIComponent.

的实现
public interface IComponent
{
    void Attach(RenderHandle renderHandle);
    Task SetParametersAsync(ParameterView parameters);
}

RenderHandle 的重要部分是:

public readonly struct RenderHandle
{
    public Dispatcher Dispatcher ....
    //....
    public void Render(RenderFragment renderFragment)
    {
        //....
        _renderer.AddToRenderQueue(_componentId, renderFragment);
        //...
    }
}    

RenderFragment 是:

public delegate void RenderFragment(RenderTreeBuilder builder);

A Renderer 管理渲染过程。它包含表示为 RenderTree 的 DOM(浏览器呈现的内容)。当 Renderer 将组件附加到 RenderTree 时,Renderer 创建一个 RenderHandle 并通过调用 Attach 将其传递给组件。 Component 使用此 RenderHandle 与 Renderer 通信。 Renderer 通过调用 SetParametersAsync.

与组件通信

通过调用 RenderHandle 上的 Render 方法并传递 RenderFragment 委托来“渲染”组件。

这是一个简单的渲染片段:

protected RenderFragment HelloWorld => (RenderTreeBuilder builder) =>
{
    builder.OpenElement(0, "div");
    builder.AddContent(1, "Hello Blazor");
    builder.CloseElement();
};

RenderHandle 上调用 Render 不会 渲染组件。它只是将渲染片段放在渲染器的队列中。当渲染器 运行s 片段时,它会检查其他组件引用的组件参数更改。它在引用参数已更改的任何组件上调用 SetParametersAsync

StateHasChanged 是一种 ComponentBase 方法。 StateHasChanged 由 Blazor UI 事件处理程序在内部调用,因此您很少需要手动调用它。如果你这样做,问问自己为什么?你的逻辑可能有问题!它看起来像这样:

var task = InvokeAsync(EventMethod);
StateHasChanged();
if (!task.IsCompleted)
{
    await task;
    StateHasChanged();
}

主要异常是一个正常的事件处理程序。如果事件更新组件中的数据,则需要通过调用 StateHasChanged 触发手动更新。这是标准模式。

private void OnSomethingChanged(object? sender, EventArgs e)
    => this.InvokeAsync(StateHasChanged);

备注:

  1. StateHasChanged 具有检测渲染片段是否已排队的机制,因此它不会对多个渲染进行排队。
  2. InvokeAsync 确保任务在 UI 线程上 运行。它使用 RenderHandle.
  3. 上提供的 Dispatcher
  4. 当渲染器检查参数更改时,任何对象都被认为是脏的,因为渲染器没有简单的方法来检查相等性。
  5. 渲染器仅在获得线程时间时才为其队列服务。如果您 运行 有一长串同步代码,比如一个按钮单击处理程序,在同步代码完成之前什么也不会发生。

如果您想进一步挖掘,请深入研究 ComponentBase - You can view the code here