为什么在Blazor wasm中使用await Task.Delay(1)?

4

许多SO答案使用await Task.Delay(1)来解决Blazor(wasm)中的各种异步渲染问题,我甚至在自己的代码中找到了一些地方,在这些地方使用该方法可以“使其正常工作”。

然而,它总是被当作事实陈述,没有经过详细的解释,在文档中也找不到这种技术。

一些问题:

  • 为什么要使用await Task.Delay(1) - 在什么情况下会使用此技术,有哪些用例?
  • 文档没有讨论这个问题(我没有找到);这是因为这是一个 hack,还是这是处理用例的合法方式?
  • Task.Delay(1)Task.Yield()之间有什么区别吗?

你能否列出一小组问题及其答案,建议使用await Task.Delay(1)作为解决方案吗? - Theodor Zoulias
@TheodorZoulias 好的,这是一个不错的方法,我会尝试编制一个有趣的地方列表,也许这会阐明这种技术。 - lonix
谢谢@aybe,这是一个有趣的帖子。我认为Blazor(wasm)特别的问题与渲染队列的工作方式有关。 - lonix
搜索 Blazor 源代码以查找出现的情况,也许你会找到一些提示:https://github.com/dotnet/aspnetcore。按下句号键在 Web 浏览器中打开 VS Code。 - aybe
@TheodorZoulias,这是给你的链接:https://dev59.com/C8Xsa4cB1Zd3GeqPh3_G#74489072 - enet
显示剩余2条评论
3个回答

7
为什么要使用 await Task.Delay(1) - 什么情况下会使用这种技术,它的用途是什么?
在你的代码中使用它可以为 UI 提供更新和重绘的机会。
文档没有讨论这个问题(至少我没有发现);这是因为它是一个 hack 吗,还是一种处理 use case 的合法方式?
这是一个 hack。但对于 Blazor/WASM 这种特定情况(没有其他 Blazor 或 .NET 运行时),没有太多其他选择。如果可能的话,建议将你的逻辑拆分,这样你的应用程序不会一次性做太多事情;但有时这并不容易(或者不可能)。
Task.Delay(1) 和 Task.Yield() 之间有什么区别吗?
根据浏览器的具体细节而定,有区别。
在 Windows UI 应用程序上,Task.Yield 不起作用,因为 UI 消息循环是一个优先级队列,“运行此代码”是最高优先级。所以(再次强调,对于 Windows UI 应用程序),这会排队执行其余的方法,然后返回到消息循环,消息循环会继续执行代码而不是刷新 UI(这些是低优先级消息)。
对于 Blazor/WASM,无论 Task.Yield 是否可用都取决于浏览器实现其(隐式)消息循环的方式。如果它有类似的优先级队列,那么你将面临与 Windows UI 相同的问题,即 Task.Yield 的确会让出消息循环,但不会耗尽它。
在所有平台上,Task.Delay(1) 实际上是将一个定时器回调排队,这通常足以在代码继续运行之前处理一些 UI 更新。

2
英译中:
为什么要使用 await Task.Delay(1)?
为了在事件处理程序中显示中间结果。
文档没有讨论这个问题。通常情况下,这不是必需的。但是也没有反对使用它的理由。当我解决类似这样的问题时,我发现如何使用它。而且我得到了一些负面反馈,在这些帖子的评论中可以看到。
Task.Delay(1) 和 Task.Yield() 有什么区别吗?
是的,Task.Yield() 看起来更合理,但我发现它并不总是有效。请参见 Stephen Cleary 在这里的答案。

1
“不要仅仅依赖于await调用的异步性,并使用Task.Delay(1)来保证它。” -- 我认为Task.Delay(1)并不能保证任何事情。理论上,它可能没有任何效果。当前线程在创建了Task.Delay(1)任务后可能会被操作系统挂起,当它几毫秒后恢复时,可能会发现Task已经完成。在这种情况下,await将同步完成,代码的行为就像遇到了await Task.CompletedTask一样。虽然我不知道这种情况是否实际上可能发生。 - Theodor Zoulias
1
所以原来是你搞的啊!?它应该有个名字...“Henk的黑客”也许?它在很多地方对我都很有用(必需),但直到现在我才明白为什么。谢谢Henk! - lonix
1
刚刚发现文档中有类似于“异步处理程序涉及多个异步阶段”的内容。 - lonix

0
以下是我和 Henk Holterman 共同回答的一个问题的链接,他在回答中使用了 await Task.Delay(1);
运行代码并查看差异,例如使用 await Task.Delay(1); 会导致组件重新渲染两次等。
使用 await Task.Delay(1); 是必要的吗? 绝对不是。这是一种不良实践,不仅会导致组件再次重新渲染,而且可能会在复杂代码中引起微妙的问题。Blazor 提供了一系列生命周期方法,您可以捕获并使用它们来提供所需的解决方案。请不要进行黑客攻击。长期来看,这可能非常昂贵。创建优雅的代码,而不是黑客...

更新

下面的代码片段描述了使用Task.Delay的用例,演示了一个带有标题为“保存”的按钮元素的页面。要求是在用户单击按钮后立即将标题文本更改为“正在保存...”,在将员工记录保存到数据存储中的过程中持续显示。如果您知道其他Task.Delay的用例,请告诉我。

Index.razor

@page "/"

<div>
    <button class="btn btn-primary" 
                              @onclick="Save">@caption</button>
</div>


@code 
{
    
    private string caption = "Save";

    private async Task SaveEmployee()
    {
        // Simulate saving an employee's record in database...
        // I use System.Threading.Thread.Sleep instead of a loop 
        // with millions of iterations.
        System.Threading.Thread.Sleep(3000);
        // Retruns completed task
        await Task.CompletedTask;
    }

    private async Task Save()
    {
        caption = "Saving...";
        // Renders here automatically after calling Task.Delay()
            
        await Task.Delay(1000);

        await SaveEmployee();

        caption = "Save";
        // Renders here automatically when SaveEmployee() 
        //complete
    }
}

另一方面,以下代码片段演示了如何不使用 Task.Delay 并提供了一种优雅的解决方案,这绝对不是hack,并且其额外的优势在于它导致组件的单一渲染...请注意: Task.Delay 涉及第二次渲染...

注意:下面的代码是对此问题的回答。

Component.razor

<div @ref="ReferenceToDiv" id="select-@Id" style="background-color: red; width:300px; height: 300px">

 </div>

@code
{
    ElementReference ReferenceToDiv;
    // As you can see, you should call the "adjustPosition" method from the 
    // `OnAfterRenderAsync` method to ensure that the div element has been 
    // rendered. DO Not Re-render In Vain. That is, do not use
    // await Task.Delay(1); to re-render your component

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (opened)
        {
          await jsModule.InvokeVoidAsync("adjustPosition", ReferenceToDiv);                
        } 
    }

    public void OnClick()
    {
        opened = !opened;
       
    }
}

test.js

export function adjustPosition(element) {
    // Should return 300px
    console.log($(element.style.width);   
}

感谢enet提供的观点。我已经尽力避免使用这种技术,但在多个场合中,这是唯一有效的方法——让渲染器按照渲染队列上的作业进行工作。下次我再遇到这种情况,我会在这里联系你,并提供一个单独问题的链接,也许你可以向我展示另一种解决方法。 - lonix
1
如果像你所说的那样,使用await Task.Delay(1);是不好的实践,那么请展示你的index.razor使用正确的实践。 - mxmissile

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接