Blazor组件StateHasChanged触发后OnInitialized重复执行问题排查
Blazor组件StateHasChanged后重复触发OnInitialized的原因分析
我们开发Blazor时碰到一个异常:名为View的组件,在根组件调用StateHasChanged后会被重新创建,导致OnInitialized方法重复执行。按照Blazor的设计,这个方法本该只在组件首次创建时执行一次,而且View组件已经存储在栈类型的私有字段_navStack中。
有问题的根组件Razor代码
@foreach (var rec in _navStack) { <div class="viewport" style="height : calc(100vh - 104px)"> <div @key="rec.Id" class="h-100 @(rec == _navStack.Peek() ? "d-block" : "d-none")"> @rec.Fragment </div> </div> }
问题场景
用户点击按钮触发根组件调用StateHasChanged后,foreach代码块执行,渲染rec.Fragment(即View组件)时,其OnInitialized方法被再次执行。
修复后的代码
把viewport容器移到foreach外部后,问题消失:
<div class="viewport" style="height : calc(100vh - 104px)"> @foreach (var rec in _navStack) { <div @key="rec.Id" class="h-100 @(rec == _navStack.Peek() ? "d-block" : "d-none")"> @rec.Fragment </div> } </div>
相关CSS样式
.viewport { min-height: 100%; padding: 0 10px; overflow: clip; } .d-block { display: block !important; } .d-none { display: none !important; }
问题原因
核心在于Blazor的虚拟DOM diff算法的节点上下文识别逻辑:
- 当
viewportdiv放在foreach内部时,每个循环项都会生成一个独立的父容器。Blazor对比虚拟DOM时,会将每个循环项的父元素视为不同的节点上下文。 - 虽然给内部包裹Fragment的div加了
@key="rec.Id",但这个key是相对于它的直接父节点(foreach里的viewport div)生效的。根组件触发重渲染时,Blazor会重新遍历_navStack,为每个rec重新创建viewport div的虚拟节点,此时Fragment所在的div即便有key,也因为父节点上下文改变,被Blazor判定为新的组件节点,从而销毁旧的View实例并重建,导致OnInitialized再次执行。 - 把viewport移到foreach外部后,所有循环项的div都处于同一个父节点下,
@key="rec.Id"能正确让Blazor识别每个循环项的唯一性。重渲染时,Blazor只会对比现有组件实例的状态,不会销毁重建,自然不会重复触发OnInitialized。
另外要注意:CSS的d-none只是控制元素是否显示,不会从DOM树中移除组件实例,所以移到外部后,所有View实例都保留在同一个父容器的上下文里,diff算法可以正确复用这些实例。
内容的提问来源于stack exchange,提问作者VitalySky
相关产品推荐
相关产品推荐

