在 Blazor Server 中,是否保证 js 互操作调用的顺序?

In Blazor Server, is order of js interop calls guaranteed?

鉴于

我假设有一个 onscroll 或 mousemove javascript 事件处理程序,它在服务器上调用 C# 方法:

然后

服务器保证 C# 方法调用的顺序?

例如以下 javascript:

document.body.addEventListener("scroll",(e) => {
   DotNet.invokeMethodAsync("BlazorSample", "HandleOnScroll", e)
});

和 C#

@code {
    [JSInvokable()]
    public static async Task HandleOnScroll()
    {
        // ...
    }
}

一个类似的问题会反过来,从 DotNet 调用 JS。

简短回答:是。

更长的答案:

从 C# 到 JavaScript 的调用会立即执行,反之亦然。

返回 C# 的调用然后由渲染调度程序处理,这意味着如果 C# 代码是同步的(或一直异步等待),那么每次调用 C# 都将 运行 在下一个开始。

如果 C# 代码是异步的并且不等待异步操作(例如 Task.Delay),那么渲染分派器将在当前当前调用后立即开始 运行 下一个调用的方法一个等待。

多个线程不会同时运行。渲染调度程序不会在 await 之后继续代码,直到当前调度的任务执行 await 或完成。

实际上,Render Dispatcher 序列化了对组件的访问,因此一次只有 1 个线程在任何组件上 运行ning - 不管有多少线程在它们上 运行ning。

PS:您可以对任何由外部刺激触发的代码使用 InvokeAsync(......) 来执行相同的操作,例如由另一个用户的线程在 Singleton 服务上引发的事件。

如果您想了解有关渲染调度程序如何工作的更多信息,请阅读 Blazor 大学的 Multi-threaded rendering 部分。

这里有一些证明:

首先,创建 index.js 并确保在您的 HTML 页面中使用 <script src="/whatever/index.js"></script>

引用它
window.callbackDotNet = async function (objRef, counter) {
    await objRef.invokeMethodAsync("CalledBackFromJavaScript", counter);
}

接下来,更新 Index.razor 页面,使其调用 JavaScript 并接受回调。

@page "/"
@inject IJSRuntime JSRuntime

<button @onclick=ButtonClicked>Click me</button>

@code
{
    private async Task ButtonClicked()
    {
        using (var objRef = DotNetObjectReference.Create(this))
        {
            const int Max = 10;
            for (int i = 1; i < 10; i++)
            {
                System.Diagnostics.Debug.WriteLine("Call to JS " + i);
                await JSRuntime.InvokeVoidAsync("callbackDotNet", objRef, i);
            }

            System.Diagnostics.Debug.WriteLine("Call to JS " + Max);
            await JSRuntime.InvokeVoidAsync("callbackDotNet", objRef, Max);
        }
    }

    [JSInvokable("CalledBackFromJavaScript")]
    public async Task CalledBackFromJavaScript(int counter)
    {
        System.Diagnostics.Debug.WriteLine("Start callback from JS call " + counter);
        await Task.Delay(1000).ConfigureAwait(false);
        System.Diagnostics.Debug.WriteLine("Finish callback from JS call " + counter);
    }
}

等待整个链,包括 JavaScript,因此输出将如下所示...

Call to JS 1
Start callback from JS call 1
* (one second later)
Finish callback from JS call 1
Call to JS 2
Start callback from JS call 2
* (one second later)
Finish callback from JS call 2

... etc ...

Call to JS 9
Start callback from JS call 9
* (one second later)
Finish callback from JS call 9
Call to JS 10
Start callback from JS call 10
* (one second later)
Finish callback from JS call 10

如果您从 JavaScript 中删除 asyncawait,就像这样

window.callbackDotNet = function (objRef, counter) {
    objRef.invokeMethodAsync("CalledBackFromJavaScript", counter);
}

当您 运行 它时,您会看到对 JavaScript 的调用是在正确的 1..10 顺序中,并且对 C# 的回调是在正确的 1..10 顺序中,但是"Finish callback" 顺序是 2,1,4,3,5,7,6,9,8,10。

这将是由于 C# 内部 .NET 调度等原因,其中 .NET 决定在完成所有任务后接下来要执行哪个任务 await Task.Delay(1000)

如果您在 JavaScript 中恢复 asyncawait,然后仅在最后一次 C# 调用中恢复 await,如下所示:

        using (var objRef = DotNetObjectReference.Create(this))
        {
            const int Max = 10;
            for (int i = 1; i < 10; i++)
            {
                System.Diagnostics.Debug.WriteLine("Call to JS " + i);
                _ = JSRuntime.InvokeVoidAsync("callbackDotNet", objRef, i);
            }

            System.Diagnostics.Debug.WriteLine("Call to JS " + Max);
            await JSRuntime.InvokeVoidAsync("callbackDotNet", objRef, Max);
        }

然后你会看到如下输出:

Call to JS 1
Call to JS 2
Call to JS 3
Call to JS 4
Start callback from JS call 1
Start callback from JS call 2
Start callback from JS call 3
Start callback from JS call 4
* (one second later)
Finish callback from JS call 1
Finish callback from JS call 2
Finish callback from JS call 3
Finish callback from JS call 4

注意:您的示例代码会导致多个用户同时执行一个静态方法。在您的场景中,您应该调用实例方法。