什么时候应该调用StateHasChanged,而Blazor何时会自动拦截到某些内容已更改?

76

我很难理解什么时候应该调用 StateHasChanged()以及Blazor何时拦截到某些内容已更改,因此必须重新呈现。

我创建了一个示例项目,其中包含一个按钮和一个名为AddItem的自定义组件。该组件包含一个带红色边框的div和一个按钮。

我的期望:我希望当用户单击Index页面中包含的按钮时,AddItem的div将显示出来。然后我想在用户单击AddItem的按钮时隐藏它。

注意: AddItem不会在外部公开其_isVisible标志,而是包含一个Show()方法。因此,在单击Index的按钮时,将调用AddItems.Show()

测试:

  1. 我单击了Index的点击按钮,然后调用了Open()AddItem.Show()方法。标志_isVisible设置为true,但没有任何操作,并调用了Index的ShouldRender()方法。

    控制台输出:

    • Render Index
  2. 我使用public void Show() {_isVisible = true; StateHasChanged();}修改了AddItem.Show()。现在AddItem的div按预期显示和隐藏。

    控制台输出:

    • Render AddItem (1° click on index's button)
    • Render Index (1° click on index's button)
    • Render AddItem (2° click on addItem's close button)
  3. 我使用<AddItem @ref="AddItem" CloseEventCallback="CallBack" />修改了<AddItem @ref="AddItem" />,从AddItem的Show()方法中删除了StateHasChanged。现在AddItem的div按预期显示和隐藏。

基于测试3:如果将AddItem的CloseEventCallback设置为任何父级方法时,为什么不必显式调用StateHasChanged? 我很难理解这一点,因为AddItem没有在任何地方调用CloseEventCallback

以及

Blazor何时理解某些内容已更改,因此必须重新呈现?

我的示例代码(如果你想试试)。

My Index.razor

<AddItem @ref="AddItem" />
<button @onclick="Open">click</button>
@code {
    AddItem AddItem;

    public void Open()
    {
        AddItem.Show();
    }

    public void CallBack()
    {
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("Render INDEX");
        return base.ShouldRender();
    }
}

我的 AddItem 组件

@if (_visible)
{
    <div style="width: 100px; height: 100px; border: 1px solid red">testo</div>
    <button @onclick="Close">close</button>    
}

@code {
    private bool _visible = false;

    [Parameter] public EventCallback<bool> CloseEventCallback { get; set; }

    public void Show()
    {
        _visible = true;
    }

    public void Close()
    {
        _visible = false;
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("Render ADDITEM");
        return base.ShouldRender();
    }
}
2个回答

40
通常情况下,当UI事件被触发后,比如单击按钮元素,点击事件被触发,然后StateHasChanged()方法自动被调用以通知组件其状态已更改并且应重新渲染。
当访问Index组件时,首先呈现父组件,然后呈现其子组件。
每当单击“打开”按钮时,Index组件重新呈现(这是因为事件的目标是父组件,默认情况下将重新呈现(无需使用StateHasChanged)。但是子组件不知道它的状态已更改。为了使子组件意识到其状态已更改并且它应该重新渲染,您应在Show方法中手动添加对StateHasChanged方法的调用。现在,当您单击“打开”按钮时,首先重新呈现子组件,然后再次呈现其父组件。现在,红色div被呈现为可见。
单击“关闭”按钮以隐藏红色div。这次只有子组件重新呈现(这是因为事件的目标是子组件,并默认情况下重新呈现),而不是父组件。
这种行为是正确且经过设计的。
如果从AddItem.Show方法中删除对StateHasChanged方法的调用,请定义...属性: [Parameter] public EventCallback<bool> CloseEventCallback { get; set; },并且在父组件中添加一个组件属性来为此属性分配一个值,如下所示:<AddItem @ref="AddItem" CloseEventCallback="CallBack" />,你会注意到外观上没有任何变化,但是这一次,当单击“打开”按钮时重新渲染的顺序是首先重新渲染父元素,然后重新渲染子元素。这正好描述了您在评论中提出的问题:

那么,为什么我的测试3能够如预期般工作,即使 CloseEventCallback 在任何地方都没有被调用?

你是正确的...在进一步调查之前,我无法真正解释这种行为。 我会尝试找出发生了什么,并让您知道。

AddItem 的 close 方法调用 CloseEventCallback 来通知父组件它应该重新渲染。

注意:您的代码使用布尔类型说明符定义了 CloseEventCallback,因此您必须在父组件中定义一个具有布尔参数的方法。 当您调用 CloseEventCallback “委托” 时,实际上是调用 Index.Callback 方法,并且您应该传递一个布尔值。自然而然地,如果您向组件传递了一个值,则希望它重新呈现,以便可以在 UI 中看到新状态。这就是问题发生的原因。EventCallback 提供的功能:尽管事件在子组件中触发,但其目标是父组件,这导致父组件重新渲染。

我想知道如果被订阅的 EventCallback 被调用,为什么父组件应该重新渲染自己?

这正是我试图在上面段落中解释的。EventCallback 类型是专门设计来解决事件目标问题的,将事件路由到状态已更改的组件(即父组件),并重新渲染它。


谢谢Enet,现在我更清楚了。 我还有两个问题。 1.从Blazor文档中可以预期,AddItem的close方法会调用CloseEventCallback来通知父级应该重新渲染。 那么,为什么即使没有在任何地方调用CloseEventCallback,我的测试3也能按预期工作? 2.我想知道为什么父组件应该在订阅的EventCallback之一被调用时重新呈现自身? 我可能有一个与UI状态无关的EventCallback。 我知道这是一个奇怪的问题,我只是想问因为我想理解。 - Leonardo Lurci
1
@Leonardo Lurci,我已经更新了我的答案以回答你在上面评论中的问题。这是一个非常复杂的主题...请不要犹豫,随时提问。 - enet
谢谢。我不知道渲染过程中有特定的顺序。当你写道当你调用CloseEventCallback[...]时,你期望它重新渲染时,我同意你的观点,事实上我的问题很奇怪,只是为了了解Blazor引擎下正在发生什么。如果可能的话,我想问一下你从哪里获得这些信息:是在GitHub上查看asp.net Core Blazor吗?我正在尝试学习Blazor,并且我想了解更多关于“如何使用Blazor/可以做什么与Blazor”的内容之外的东西。你有任何建议吗?如果你在我的test 3测试中发现了任何问题,请告诉我。 - Leonardo Lurci
1
我的学习来源是文档,不久以前非常贫乏,还有在stackoverflow上回答问题和在github的blazor问题部分。我不会访问其他来源,如博客,并且我不会浪费时间使用第三方组件。我纯粹集中于Blazor组件模型及其使用方法。我还从Blazor团队创建的代码示例中学到了很多,例如FlightFinder、Blazing pizzas等。 - enet
2
最好的资料由Steve Anderson提供...只需前往他的代码库,阅读他的示例,分析它们,运行它们等。这里是一个由Blazor团队成员rynowak提供的关于EventCallback的问题链接,请确保您全部阅读:https://github.com/dotnet/aspnetcore/issues/6351 - enet

10

组件必须在被父组件添加到组件层次结构时首次渲染。这是唯一必须渲染组件的时间。根据其自身的逻辑和约定,组件可能会在其他时间进行渲染。

SPA应用程序遵循以下组件架构:

a nodes-graph representing SPA architecture, with a root node and leaves, some nodes being parents and some being children

将树中的每个节点视为Blazor应用程序中我们组件的一部分。该组件具有父组件,并且可以具有子组件。
ComponentBase(任何Blazor组件的标准基类)接收到StateHasChanged的调用。这是包含触发以下时间自动重新呈现的逻辑的地方:
- 从父组件应用更新的参数集之后。 - 应用级联参数的更新值之后。 - 在通知事件并调用其自己的事件处理程序之后。 - 调用自己的StateHasChanged方法之后(请参阅ASP.NET Core Razor component lifecycle)。
与Blazor的自动呈现调用不同,在我们的情况下,呈现调用来自StateHasChanged。这具有不同的行为:
当框架本身在上述时间之一呈现时,它仅呈现其子组件(子代)。 但是,当我们调用StateHasChanged时,大多数继承自ComponentBase的组件都会被呈现,而不考虑它们在树中的位置。(不必要的重新呈现)。 因此,我们应避免使用StateHasChanged,让框架处理呈现。

在以下情况下,调用 StateHasChanged 是有意义的:

  • 涉及多个异步阶段的异步处理程序。
  • 从 Blazor 渲染和事件处理系统之外的某些外部内容接收到调用。
  • 渲染子树之外的组件,并由特定事件重新呈现。

了解更多信息:


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