避免为异步方法重新创建事件委托

Avoid event delegate recreation for async methods

我有一个 Blazor Server 应用程序,其中包含许多在 for 循环中呈现的按钮。:

@for (int i = 0; i < _navItems.Count; i++)
{
  var localI = i;
  <div class="col-3 mb-2">
     <button @onclick="async () => await SetCurrentAsync(localI)" class="btn btn-sm">
       @(i + 1)
     </button>
  </div>
}

但是,Microsoft Docs here 不推荐这种方法,因为每次呈现组件时都会重新创建 @onclick 中指定的委托:

Blazor's recreation of lambda expression delegates for elements or components in a loop can lead to poor performance.

此后文档中提供的解决方案(以及 linked GitHub issue 中提供的解决方案是创建一个 Button 类型,其中 Action 属性 包含委托:

@foreach (var button in _buttons)
{
  <div class="col-3 mb-2">
   <button @key="button.Id" @onclick="button.Action" class="btn btn-sm">
    @(i + 1)
   </button>
  </div>
}

@code {
   
     List<Button> _buttons = new();
     List<NavItem> _items;

     protected override async Task OnInitializedAsync()
     {
        _items = await GetItemsFromDb();
        for(int i = 0; i < _items.Count; i++)
        {
            var localI = i;
            _buttons.Add(new Button 
            { 
               Id = item.Id, 
               Action = () => SetCurrent(localI);  
            });
        }
     }

    class Button 
    {
         public int Id { get; set; }
         
         public Action Action { get; set; }
    }
}

现在,@onclick 引用 Button.Action 并解决了委托重新创建问题。

SetCurrent 不是异步之前,一切都很有趣和游戏。 Action 必须更改为 Func<Task> 并且必须使用异步 lambda 表达式添加按钮:

_buttons.Add(new Button 
{ 
  Id = item.Id, 
  Action = async () => await SetCurrentAsync(localI);  
});

我还要做:

@onclick="async() => await button.Action"

这将再次重新创建委托。 我该如何为异步方法执行此操作?

首先,做这样的事情

@onclick="async () => await SetCurrentAsync(localI)"

不需要。您正在将任务包装在任务中。

@onclick="()=> SetCurrentAsync(localI)"

效果完全一样。 Blazor 组件内部事件处理程序(用于单击按钮...)包装您在任务中传递的任何操作。最简单的看起来像这样:

var task = InvokeAsync(EventMethod);
StateHasChanged();
if (!task.IsCompleted)
{
    await task;
    StateHasChanged();
}

您应该始终使用 Func<Task> 来处理 sync TaskTask。将 Action 与异步方法一起使用是不行的 - 它 return 是 Blazor 组件事件处理程序的 void

有关工作演示,请参阅下面的代码页。共有三个使用模式

的按钮
  1. 首先使用异步任务调用一个产生任务的方法。
  2. 第二次调用一个简单的任务方法。
  3. 使用 Actionasync void 并演示 UI 更新问题。

关键是前两个都 return Task 到 Blazor 组件事件处理程序,因此它可以正确处理组件呈现。

@page "/"
<h3>Button Actions</h3>

@foreach (var item in _buttonActions)
{
    <div class="m-2">
        <button class="btn btn-secondary" @onclick="item.Action">@item.Title</button>
    </div>
}
<div>
    @message
</div>

@code {
    private List<ButtonAction> _buttonActions = new List<ButtonAction>();

    protected override void OnInitialized()
    {
        {
            var item = new ButtonAction() { Title = "Task" };
            item.Action = () => GetValue(item);
            _buttonActions.Add(item);
        }
        {
            var item = new ButtonAction() { Title = "Async Task" };
            item.Action = () => GetValueAsync(item);
            _buttonActions.Add(item);
        }
        {
            var item = new ButtonAction() { Title = "Async Void Task" };
            item.MyAction = () => GetValueVoidAsync(item);
            _buttonActions.Add(item);
        }
    }

    private string message;

    public Task GetValue(ButtonAction item)
    {
        message = $"Value: {item.Id}";
        return Task.CompletedTask;
    }

    public async Task GetValueAsync(ButtonAction item)
    {
        await Task.Yield();
        message = $"Value: {item.Id}";
    }

    public async void GetValueVoidAsync(ButtonAction item)
    {
        await Task.Yield();
        message = $"Value: {item.Id}";
    }

    public class ButtonAction
    {
        public string Title { get; set; }
        public Guid Id { get; } = Guid.NewGuid();
        public Func<Task> Action { get; set; }
        public Action MyAction { get; set; }
    }
}

关于性能,我认为这真的取决于“有多少?”。我不使用编辑列表的模式,因为我可能有 25 个编辑和查看按钮。我从来没有注意到一个问题。