延迟的 OnParametersSetAsync 任务触发子组件的重复 OnParametersSet
Delayed OnParametersSetAsync task triggers duplicate OnParametersSet of child components
当父 page/component 有一个“长” 运行 OnParametersSet 事件时,我试图避免在子组件中触发重复的 OnParametersSet 事件。例如,这是一个包含一些子组件的基本页面。
@page "/test"
<Node>
<Node>
<Node></Node>
</Node>
</Node>
@code {
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Page: OnInitializedAsync - start");
await Task.Delay(2000);
Console.WriteLine("Page: OnInitializedAsync - finish");
}
}
节点组件也很简单:
<div>Node: @GetHashCode()</div>
@ChildContent
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
protected override void OnParametersSet()
{
Console.WriteLine("Node {0}: OnParametersSet", GetHashCode());
}
}
这是我在控制台中看到的内容。请注意,在页面完成其 OnParametersSet 事件后,三个子组件中的两个会再次调用 OnParametersSet。
Page: OnInitializedAsync - start
Node 924945978: OnParametersSet
Node 1026183343: OnParametersSet
Node 213373360: OnParametersSet
Page: OnInitializedAsync - finish
Node 924945978: OnParametersSet
Node 1026183343: OnParametersSet
这只是 Blazor 的缺陷还是有更好的方法来避免这些额外事件?节点组件与页面本身的内容无关。在现实世界中,我可能有很多子组件,每个子组件都试图异步获取数据,所以我想防止触发这些额外事件。
我发现的唯一解决方法是用 @if(pageSetParametersEventHasFinished) 语句包装节点块,以防止节点组件在页面“准备好”之前进行初始化。
这是设计使然。请注意,2 个外部节点渲染了两次,因为它们具有 ChildContent。内节点已经是'stable'.
Blazor 'errs on the safe side',它不能保证 ChildContent 中没有任何变化。
但是OnParametersSet是(应该是)轻操作,本身不是问题。担心它后面的 Render 动作。
我的参数最佳实践
- 在组件中保留一份副本
- 只有当新值与副本不同时,才做fetch-data或其他工作
- 对于重组件,使用 ShouldRender() 来最小化 re-rendering。
看起来像这样
bool shouldRender = true;
protected override void OnAfterRender(bool firstRender)
{
...
shouldRender = false;
}
protected override bool ShouldRender()
{
return shouldRender;
}
protected override void OnParametersSet()
{
if (myCopy != Param)
{
myCopy = Param;
shouldRender = true;
... // fetch or process data
}
}
只有 'heavy' 渲染 and/or 处理数据的组件才值得付出努力。
修改后的答案
首先,您可以停止渲染 sub-components,直到使用简单的检查器加载数据。
@page "/"
@if (loaded)
{
<Node>
<Node>
<Node></Node>
</Node>
</Node>
}
@code {
private bool loaded;
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Page: OnInitializedAsync - start");
await Task.Delay(2000);
loaded = true;
Console.WriteLine("Page: OnInitializedAsync - finish");
}
}
在组件渲染事件期间,Renderer
在任何 sub-component 上调用 SetParametersAsync
,其中一个参数已更改,或者其中一个参数是对象。没有标准的对象相等性检查器。
ChildContent
和任何其他 RenderFragment
是特例。这些是委托 - 对在父上下文中执行的父代码块的引用。如果父级需要渲染,那么任何引用 RenderFragment
委托的组件也需要渲染。所以渲染器总是 re-renders sub-components 包含有效的 RenderFragments
.
在您的代码中:
<Node>
<Node>
<Node></Node>
</Node>
</Node>
<Node>
<Node></Node>
</Node>
是一个 RenderFragment
这就是为什么外部和中间 Nodes
得到 re-rendered.
如果您没有 RenderFragements
,那么您可以使用 Record
将数据传递到组件并进行手动相等性检查。
以下代码对此进行了演示:
保存数据的记录
public record MyComponentData
{
public string? Message { get; set; }
}
带有内联注释的简单组件
<div class="m-s p-2">
Data : @this.ComponentData.Message
</div>
<div class="m-s p-2">
On Params Called : @this.OnParamsCalled
</div>
@code {
[Parameter] public MyComponentData ComponentData { get; set; } = new();
private MyComponentData CurrentComponentData { get; set; } = new();
private int OnParamsCalled = 0;
private bool isInit = true;
// SetParametersAsync is the method the Renderer calls on the Component to init an update
public async override Task SetParametersAsync(ParameterView parameters)
{
// ALWAYS do this first - it releases the ParameterView back to the Renderer
parameters.SetParameterProperties(this);
// As we are using records we can do a equality check
var isupdate = this.CurrentComponentData != this.ComponentData;
if (isInit | isupdate)
{
// If this is the first time or we have a record change runs the full ComponentBase process
// by calling the base SetParametersAsync.
// Note we pass it ParameterView.Empty as we have already set the parameters in the first line
await base.SetParametersAsync(ParameterView.Empty);
// Set the record to the new record
this.CurrentComponentData = ComponentData;
}
// set IsInit to false
isInit = false;
}
protected override void OnParametersSet()
{
OnParamsCalled++;
Debug.WriteLine("Component: OnParametersSet - called ");
base.OnParametersSet();
}
}
还有一个测试页:
@page "/"
<MyComponent ComponentData=this.myComponentData />
<div class="m-2">
<button class="btn btn-success" @onclick=UpdateData>Update Message</button>
<button class="btn btn-danger" @onclick=UpdateNothing>Update Message</button>
</div>
@code {
private MyComponentData myComponentData = new MyComponentData();
private void UpdateData()
{
myComponentData = myComponentData with { Message = $"Updated at {DateTime.Now.ToLongTimeString()}" };
}
private async Task UpdateNothing()
{
await Task.Delay(500);
}
protected override async Task OnInitializedAsync()
{
Debug.WriteLine("Page: OnInitializedAsync - start");
await Task.Delay(2000);
Debug.WriteLine("Page: OnInitializedAsync - finish");
}
}
原答案
Is this just a flaw with Blazor
为什么有缺陷?您是在暗示 Blazor 的基本设计存在缺陷吗?
产生的任何 OnInitializedAsync
都有两个渲染事件。
- 当
OnInitializedAsync
中的方法产生时。
- 当
OnParametersSetAsync
完成时。
这是设计使然。通常这用于在组件中显示“正在加载”消息,直到加载与该方法关联的数据。
渲染进程只是更新 DOM 的渲染器版本。只有对 DOM 的更改才会传输到浏览器 DOM.
使用如下处理参数更改的抽象组件。
public abstract class WatchComponent : ComponentBase
{
private bool _isDirty = true;
private bool _shouldRender = true;
protected abstract Task OnParametersChangedAsync();
protected override async Task OnParametersSetAsync()
{
if (!_isDirty) return;
await OnParametersChangedAsync();
_isDirty = false;
}
protected override void OnAfterRender(bool firstRender)
{
_shouldRender = false; // disable if child component has RenderFragment parameter
Console.WriteLine("{0}: OnAfterRender", GetHashCode());
base.OnAfterRender(firstRender);
}
protected override bool ShouldRender() => _shouldRender;
protected void SetField<T>(ref T field, T value)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
Console.WriteLine("parameter changed from {0} to {1}", field, value);
field = value;
_isDirty = true;
_shouldRender = true;
}
}
将它与上面的演示一起使用,我尝试了以下操作,一切似乎都有效。
更新页面:
@page "/test"
<Node Value="@_value1">
<Node Value="@_value2">
<Node></Node>
</Node>
</Node>
@code {
private string _value1 = "v1";
private string _value2 = "v2";
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Page: OnInitializedAsync - start");
await Task.Delay(2000);
Console.WriteLine("Page: OnInitializedAsync - finish");
_value1 = "v1.1";
_value2 = "v2";
}
}
更新节点组件:
@inherits WatchComponent
<div>Node: @GetHashCode(), Value="@Value"</div>
@ChildContent
@code {
private string _value;
[Parameter]
public string Value
{
get => _value;
set => SetField(ref _value, value);
}
[Parameter] public RenderFragment ChildContent { get; set; }
protected override Task OnParametersChangedAsync()
{
// load something async here
return Task.CompletedTask;
}
}
控制台输出:
Page: OnInitializedAsync - start
parameter changed from to v1
parameter changed from to v2
833522111: OnAfterRender
854912238: OnAfterRender
668729564: OnAfterRender
Page: OnInitializedAsync - finish
parameter changed from v1 to v1.1
833522111: OnAfterRender
当父 page/component 有一个“长” 运行 OnParametersSet 事件时,我试图避免在子组件中触发重复的 OnParametersSet 事件。例如,这是一个包含一些子组件的基本页面。
@page "/test"
<Node>
<Node>
<Node></Node>
</Node>
</Node>
@code {
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Page: OnInitializedAsync - start");
await Task.Delay(2000);
Console.WriteLine("Page: OnInitializedAsync - finish");
}
}
节点组件也很简单:
<div>Node: @GetHashCode()</div>
@ChildContent
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
protected override void OnParametersSet()
{
Console.WriteLine("Node {0}: OnParametersSet", GetHashCode());
}
}
这是我在控制台中看到的内容。请注意,在页面完成其 OnParametersSet 事件后,三个子组件中的两个会再次调用 OnParametersSet。
Page: OnInitializedAsync - start
Node 924945978: OnParametersSet
Node 1026183343: OnParametersSet
Node 213373360: OnParametersSet
Page: OnInitializedAsync - finish
Node 924945978: OnParametersSet
Node 1026183343: OnParametersSet
这只是 Blazor 的缺陷还是有更好的方法来避免这些额外事件?节点组件与页面本身的内容无关。在现实世界中,我可能有很多子组件,每个子组件都试图异步获取数据,所以我想防止触发这些额外事件。
我发现的唯一解决方法是用 @if(pageSetParametersEventHasFinished) 语句包装节点块,以防止节点组件在页面“准备好”之前进行初始化。
这是设计使然。请注意,2 个外部节点渲染了两次,因为它们具有 ChildContent。内节点已经是'stable'.
Blazor 'errs on the safe side',它不能保证 ChildContent 中没有任何变化。
但是OnParametersSet是(应该是)轻操作,本身不是问题。担心它后面的 Render 动作。
我的参数最佳实践
- 在组件中保留一份副本
- 只有当新值与副本不同时,才做fetch-data或其他工作
- 对于重组件,使用 ShouldRender() 来最小化 re-rendering。
看起来像这样
bool shouldRender = true;
protected override void OnAfterRender(bool firstRender)
{
...
shouldRender = false;
}
protected override bool ShouldRender()
{
return shouldRender;
}
protected override void OnParametersSet()
{
if (myCopy != Param)
{
myCopy = Param;
shouldRender = true;
... // fetch or process data
}
}
只有 'heavy' 渲染 and/or 处理数据的组件才值得付出努力。
修改后的答案
首先,您可以停止渲染 sub-components,直到使用简单的检查器加载数据。
@page "/"
@if (loaded)
{
<Node>
<Node>
<Node></Node>
</Node>
</Node>
}
@code {
private bool loaded;
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Page: OnInitializedAsync - start");
await Task.Delay(2000);
loaded = true;
Console.WriteLine("Page: OnInitializedAsync - finish");
}
}
在组件渲染事件期间,Renderer
在任何 sub-component 上调用 SetParametersAsync
,其中一个参数已更改,或者其中一个参数是对象。没有标准的对象相等性检查器。
ChildContent
和任何其他 RenderFragment
是特例。这些是委托 - 对在父上下文中执行的父代码块的引用。如果父级需要渲染,那么任何引用 RenderFragment
委托的组件也需要渲染。所以渲染器总是 re-renders sub-components 包含有效的 RenderFragments
.
在您的代码中:
<Node>
<Node>
<Node></Node>
</Node>
</Node>
<Node>
<Node></Node>
</Node>
是一个 RenderFragment
这就是为什么外部和中间 Nodes
得到 re-rendered.
如果您没有 RenderFragements
,那么您可以使用 Record
将数据传递到组件并进行手动相等性检查。
以下代码对此进行了演示:
保存数据的记录
public record MyComponentData
{
public string? Message { get; set; }
}
带有内联注释的简单组件
<div class="m-s p-2">
Data : @this.ComponentData.Message
</div>
<div class="m-s p-2">
On Params Called : @this.OnParamsCalled
</div>
@code {
[Parameter] public MyComponentData ComponentData { get; set; } = new();
private MyComponentData CurrentComponentData { get; set; } = new();
private int OnParamsCalled = 0;
private bool isInit = true;
// SetParametersAsync is the method the Renderer calls on the Component to init an update
public async override Task SetParametersAsync(ParameterView parameters)
{
// ALWAYS do this first - it releases the ParameterView back to the Renderer
parameters.SetParameterProperties(this);
// As we are using records we can do a equality check
var isupdate = this.CurrentComponentData != this.ComponentData;
if (isInit | isupdate)
{
// If this is the first time or we have a record change runs the full ComponentBase process
// by calling the base SetParametersAsync.
// Note we pass it ParameterView.Empty as we have already set the parameters in the first line
await base.SetParametersAsync(ParameterView.Empty);
// Set the record to the new record
this.CurrentComponentData = ComponentData;
}
// set IsInit to false
isInit = false;
}
protected override void OnParametersSet()
{
OnParamsCalled++;
Debug.WriteLine("Component: OnParametersSet - called ");
base.OnParametersSet();
}
}
还有一个测试页:
@page "/"
<MyComponent ComponentData=this.myComponentData />
<div class="m-2">
<button class="btn btn-success" @onclick=UpdateData>Update Message</button>
<button class="btn btn-danger" @onclick=UpdateNothing>Update Message</button>
</div>
@code {
private MyComponentData myComponentData = new MyComponentData();
private void UpdateData()
{
myComponentData = myComponentData with { Message = $"Updated at {DateTime.Now.ToLongTimeString()}" };
}
private async Task UpdateNothing()
{
await Task.Delay(500);
}
protected override async Task OnInitializedAsync()
{
Debug.WriteLine("Page: OnInitializedAsync - start");
await Task.Delay(2000);
Debug.WriteLine("Page: OnInitializedAsync - finish");
}
}
原答案
Is this just a flaw with Blazor
为什么有缺陷?您是在暗示 Blazor 的基本设计存在缺陷吗?
产生的任何 OnInitializedAsync
都有两个渲染事件。
- 当
OnInitializedAsync
中的方法产生时。 - 当
OnParametersSetAsync
完成时。
这是设计使然。通常这用于在组件中显示“正在加载”消息,直到加载与该方法关联的数据。
渲染进程只是更新 DOM 的渲染器版本。只有对 DOM 的更改才会传输到浏览器 DOM.
使用如下处理参数更改的抽象组件。
public abstract class WatchComponent : ComponentBase
{
private bool _isDirty = true;
private bool _shouldRender = true;
protected abstract Task OnParametersChangedAsync();
protected override async Task OnParametersSetAsync()
{
if (!_isDirty) return;
await OnParametersChangedAsync();
_isDirty = false;
}
protected override void OnAfterRender(bool firstRender)
{
_shouldRender = false; // disable if child component has RenderFragment parameter
Console.WriteLine("{0}: OnAfterRender", GetHashCode());
base.OnAfterRender(firstRender);
}
protected override bool ShouldRender() => _shouldRender;
protected void SetField<T>(ref T field, T value)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
Console.WriteLine("parameter changed from {0} to {1}", field, value);
field = value;
_isDirty = true;
_shouldRender = true;
}
}
将它与上面的演示一起使用,我尝试了以下操作,一切似乎都有效。
更新页面:
@page "/test"
<Node Value="@_value1">
<Node Value="@_value2">
<Node></Node>
</Node>
</Node>
@code {
private string _value1 = "v1";
private string _value2 = "v2";
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Page: OnInitializedAsync - start");
await Task.Delay(2000);
Console.WriteLine("Page: OnInitializedAsync - finish");
_value1 = "v1.1";
_value2 = "v2";
}
}
更新节点组件:
@inherits WatchComponent
<div>Node: @GetHashCode(), Value="@Value"</div>
@ChildContent
@code {
private string _value;
[Parameter]
public string Value
{
get => _value;
set => SetField(ref _value, value);
}
[Parameter] public RenderFragment ChildContent { get; set; }
protected override Task OnParametersChangedAsync()
{
// load something async here
return Task.CompletedTask;
}
}
控制台输出:
Page: OnInitializedAsync - start
parameter changed from to v1
parameter changed from to v2
833522111: OnAfterRender
854912238: OnAfterRender
668729564: OnAfterRender
Page: OnInitializedAsync - finish
parameter changed from v1 to v1.1
833522111: OnAfterRender