为何调用5次StateHasChanged后OnAfterRenderAsync触发5次?与Blazor文档不符
Blazor官方文档指出:
如果组件在循环中连续调用5次StateHasChanged,组件只会渲染一次。
但实际测试中发现,调用5次StateHasChanged后,OnAfterRenderAsync会被触发5次,与文档描述不符。
问题
上述.NET文档中的表述是否缺失生效所需的上下文?若缺失,具体是什么?文档是否需要补充该上下文?
最小可复现示例
IStateChangeNotifyingViewModel.cs
namespace MyApp.ViewModels; public interface IStateChangeNotifyingViewModel { event Action? CallbackStateChanged; }
IFooViewModel.cs
namespace MyApp.ViewModels; public interface IFooViewModel : IStateChangeNotifyingViewModel { string FooProp { get; set; } }
FooViewModel.cs
namespace MyApp.ViewModels; public class FooViewModel : IFooViewModel { public event Action? CallbackStateChanged; private string _fooProp = string.Empty; public string FooProp { get => _fooProp; set { _fooProp = value; // 期望这些调用被批处理... CallbackStateChanged?.Invoke(); CallbackStateChanged?.Invoke(); CallbackStateChanged?.Invoke(); CallbackStateChanged?.Invoke(); CallbackStateChanged?.Invoke(); } } }
MVVMViewBase.cs
using Microsoft.AspNetCore.Components; using MyApp.ViewModels; namespace MyApp.Views; public abstract class MVVMViewBase<TViewModel> : ComponentBase, IDisposable where TViewModel : class, IStateChangeNotifyingViewModel { [Parameter] public TViewModel vm { get; set; } = default!; protected override Task OnInitializedAsync() { vm.CallbackStateChanged += StateHasChanged; return Task.CompletedTask; } public void Dispose() { vm.CallbackStateChanged -= StateHasChanged; GC.SuppressFinalize(this); } }
FooView.razor
@using MyApp.ViewModels @page "/foo" @inherits MVVMViewBase<IFooViewModel> @code { // 仅用于测试的代码块... protected override Task OnAfterRenderAsync(bool firstRender) { // 此处!调用次数与StateHasChanged的调用次数一致。 System.Console.WriteLine($"OnAfterRenderAsync called at {DateTime.Now:HH:mm:ss.fff}"); return Task.CompletedTask; } } <div> Hello world </div>
测试环境
- Blazor WebAssembly
- .NET 8
问题分析与结论
文档表述确实缺失关键上下文
文档中提到的「连续调用5次StateHasChanged仅触发一次渲染」的行为,仅在Blazor框架托管的同步执行上下文中生效,具体包括:
- 组件的生命周期方法(如
OnInitializedAsync、OnParametersSetAsync) - Blazor绑定的UI事件回调(如按钮点击、输入框变更事件)
- 通过
InvokeAsync调度到Blazor同步上下文的代码块
在本次测试的MVVM场景中,StateHasChanged是通过ViewModel的事件直接调用的——ViewModel属于组件外部的普通.NET对象,其触发的事件调用不在Blazor的批次渲染托管范围内,因此每次StateHasChanged都会直接请求Renderer发起独立渲染,导致OnAfterRenderAsync被触发5次。
文档需要补充该上下文
当前文档的表述过于笼统,没有明确说明批处理行为的生效范围,容易让采用MVVM等分离架构的开发者产生误解。建议文档补充说明:仅当StateHasChanged在Blazor框架托管的同步执行上下文中连续调用时,才会被合并为一次渲染;从外部对象直接调用时不触发批处理。
解决方案
修改MVVMViewBase中的事件绑定逻辑,通过InvokeAsync将StateHasChanged的调用切换到Blazor的同步上下文:
protected override Task OnInitializedAsync() { vm.CallbackStateChanged += () => InvokeAsync(StateHasChanged); return Task.CompletedTask; }
这样即使ViewModel同步触发多次事件,InvokeAsync会将这些调用排入Blazor的上下文队列,自动合并为一次渲染,符合文档描述的预期行为。
内容的提问来源于stack exchange,提问作者Ash

