Blazor StateHasChanged 和 child 参数(`await Task.Run(StateHasChanged);` vs `await InvokeAsync(StateHasChanged);`)

Blazor StateHasChanged and child parameters (`await Task.Run(StateHasChanged);` vs `await InvokeAsync(StateHasChanged);`)

我最近问了一个关于 Blazor wasm await Task.Run(StateHasChanged);await InvokeAsync(StateHasChanged); 之间的区别的问题

结论是await Task.Run(StateHasChanged);不正确,应该避免;使用它会产生与 await InvokeAsync(StateHasChanged); 相同的结果,但是当线程可用时会失败(接受的答案有详细解释)。

我已经更新了我的代码库以使用 await InvokeAsync(StateHasChanged);,但是我发现两者之间的结果实际上存在差异。

这是我的应用程序中问题的最小再现:

Parent

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" @ref="Child"></Child>

@code {
    private bool Bool = false;
    private Child Child;
    private string title;

    private async void SetBool(bool name)
    {
        Bool = name;
        title = Bool ? "True" : "False";
        // NOTE: This will work as expected; Child will be updated with the Parent
        // await Task.Run(StateHasChanged);
        // NOTE: This will only update the Child the second time it is clicked
        await InvokeAsync(StateHasChanged);
        await Child.Trigger();
    }

}

Child

<h3>Child: @title</h3>

@code {
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title;

    public async Task Trigger()
    {
        title = Bool ? "True" : "False";
        await InvokeAsync(StateHasChanged);
    }
}

单击 parent 中的 TrueFalse 按钮应更新 parent 和 child 中的 Bool 值.请注意,title 变量仅用于 Bool 值的可视化显示。

使用await Task.Run(StateHasChanged);会导致parent和child的状态同时更新。另一方面,await InvokeAsync(StateHasChanged); 将更新 parent,但不会更新 child;需要单击两次 a 按钮才能在 child 组件中获取相应的值。

我将这个值传递给 child 组件的方式有问题吗?

请注意,使用 await Task.Run(StateHasChanged); 不是一个选项;这样做意味着我无法使用 bUnit 测试组件。

复制代码可用here

您没有将新值传递到 Child 组件。这是您可以使其发挥作用的一种方式。

private async void SetBool(bool name)
{
    ...
                
    await InvokeAsync(StateHasChanged);
    await Child.Trigger(Bool); // pass the new value
}

public async Task Trigger(bool newValue)
{
    title = newValue ? "True" : "False";
    await InvokeAsync(StateHasChanged);
}

您在 async void SetBool(bool name) 中有一个 async void。这解释了您在 Task.Run.

中看到的差异
  1. 在 Blazor 中,您(几乎)永远不需要 async void。 Blazor 支持可等待的事件处理程序。使用 async Task SetBool(bool name) ... 结果应该是确定的。

  2. 此处不需要 InvokeAsync()。一切都在主线程上运行。
    在由 Task.Run() 执行的代码中,您只需要在 Blazor Server 中使用它。或者在 async void 中,但这不应该发生。

  3. 您不需要到处都需要 StateHasChanged()。仅使用它来显示方法中的中间结果。在示例代码中,您可能会在 Trigger 中需要它,但目前不需要 Trigger() 本身。 Child 应该在 Bool 更改时重新呈现而无需您的任何帮助。 async void 可能让您不这么认为。

你太努力了。整个样本可以简化为

Parent

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" ></Child>

@code {
    private bool Bool = false;
   // private Child Child;
    private string title;

    private void SetBool(bool name)  // no async
    {
        Bool = name;   // Child will rerender if this is a change
        title = Bool ? "True" : "False";
    }  // auto StateHasChanged

}

Child

<h3>Child: @title</h3>

@code 
{
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title => Bool ? "True" : "False";
}

以下代码片段描述了您应该如何编码:

Parent.razor

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" @ref="Child"></Child>

@code {
    private bool Bool = false;
    private Child Child;
    private string title;

    protected override void OnInitialized()
    {
        title = Bool ? "True" : "False";
    }

    private async Task SetBool(bool name)
    {
        Bool = name;
        title = Bool ? "True" : "False";
        
        await Task.CompletedTask;
    }

}

Child.razor

<h3>Child: @title</h3>

@code {
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title;

    private bool _Bool;

    public async Task Trigger()
    {
        title = _Bool ? "True" : "False";
        await InvokeAsync(StateHasChanged);
    }

    protected override async Task OnParametersSetAsync()
    {
        _Bool = Bool;
        await Trigger();
      
    }
}

注意:您遇到的问题不是因为使用 Task.Run(StateHasChanged); and await InvokeAsync(StateHasChanged);,尽管结果不同。您只是不遵守 Blazor 组件模型规则。

  1. 您是否意识到,当您单击 SetBool 按钮时,Parent 组件变为 re-rendered,因为它的状态已更改。因此,Child 组件是 re-rendered 且 Bool == true,但是您在屏幕上看不到它,因为更改 child 组件中的标题的代码位于触发器方法,仅从 Parent 组件调用。你的假设是错误的。

  2. 请勿修改或更改 Bool 参数的状态 属性。定义一个局部变量来存储参数 属性 的值,您可以随意操作其值。换句话说,组件参数属性应定义为自动属性...它们是 Blazor 框架的 DTO,用于将值从 parent 组件传递到其 children.

  3. 不要不必要地从 UI 事件处理程序调用 StateHasChanged 方法。它是自动调用的。

  4. 不要使用 async void... 使用 async Task. 当您使用 async void 时,Blazor 无法确定异步方法何时完成,并且因此它不会调用 StateHasChanged 方法

  5. 避免使用 Task.Run。我从不使用。我什至不记得为什么我不应该 用它。我只是不使用它。

await Task.Run(StateHasChanged);

是一个NONO。它说,线程池线程上的 运行 StateHasChanged,这与现实完全相反 - StateHasChanged 必须是 UI 线程上的 运行。

它在单线程环境中“有效”,因为 Task.Run 不切换线程 - 只有 UI 线程。目前在 Web Assembly 中就是这种情况。 运行 它在任何多线程环境中 - Blazor 服务器或 bUnit 测试 - 并且它会爆炸。

Blazor 在 ComponentBase 上提供了两个 InvokeAsync 方法,在 UI 线程上提供了 运行 方法。一个用于 Action,一个用于 Func。这是 Action 版本。

protected Task InvokeAsync(Action workItem)
    => _renderHandle.Dispatcher.InvokeAsync(workItem);

RenderHandle 是 Renderer 在调用 Attach 时传递给组件的 structDispatcher 是 UI 线程的线程调度程序。 InvokeAsync 确保它通过的任何 ActionFunc,它在 UI 线程上得到 运行。在我们的例子中 StateHasChanged.

上面的 Enet 和 Henk 都回答了代码中行为差异的问题。我只想重申 Enet 对 asyncvoid 的评论。我个人的规则是:在 Blazor 组件事件处理程序中不要将 asyncvoid 放在一起。

您会在此处 (Stack Overflow) 找到关于此特定主题的大量问题,以及我们三人之一的许多答案!