今天我遇到了一个问题,该组件封装了 JS 库,可能会影响 Blazor 组件的绑定值。更新
Mask
参数的值将导致 JavaScript 代码运行多次。
[Parameter]
public string Value { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Parameter]
public Mask Mask { get; set; }
private Mask _mask;
private bool _maskNeedsUpdate;
protected override void OnParametersSet()
{
// ... Handle other parameters.
if (_mask != Mask)
{
_mask = Mask;
_maskNeedsUpdate = true;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// ... initialize JS mask on first render.
if (_maskNeedsUpdate)
{
var valueAfterUpdate = await Js.InvokeAsync<string>("updateMask", _mask);
if (valueAfterUpdate != Value)
{
await ValueChanged.InvokeAsync(valueAfterUpdate);
}
_maskNeedsUpdate = false; // <-- This causes problems.
}
}
几乎就好像
ValueChanged.InvokeAsync()
调用触发了并行重新渲染,这会导致第二个 JS 互操作调用排队,因为标志仍然是 true
。但我对此表示怀疑,因为它在下一行实际上被设置为 false (同时,所有 EventCallback.InvokeAsync()
似乎所做的就是本质上在父组件中调用 StateHasChanged()
并调用其回调)。
更改代码以设置标志在调用JS之前似乎已经解决了问题。
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// ... initialize JS mask on first render.
if (_maskNeedsUpdate)
{
_maskNeedsUpdate = false; // <-- Toggle flag before awaiting JS.
var valueAfterUpdate = await Js.InvokeAsync<string>("updateMask", _mask);
if (valueAfterUpdate != Value)
{
await ValueChanged.InvokeAsync(valueAfterUpdate);
}
}
}
这个修复有点让我困惑,因为 根据文档:
Blazor 使用同步上下文 (SynchronizationContext) 来强制执行单个逻辑线程。 Blazor 引发的组件的生命周期方法和事件回调在同步上下文上执行。
Blazor 的服务器端同步上下文尝试模拟单线程环境,以便它与浏览器中的单线程 WebAssembly 模型紧密匹配。此仿真仅适用于单个电路,这意味着两个不同的电路可以并行运行。在电路内的任何给定时间点,工作都在一个线程上执行,这会产生单个逻辑线程的印象。同一电路中不会同时执行两个操作。
我的理解是,单线程模拟不应允许我的 Blazor 组件在 JavaScript 仍在运行时“并行重新渲染”(并且,查看 Blazor 源代码,最终会等待
OnAfterRenderAsync()
)。但是,为什么在 JS 互操作调用之前将标志向上移动可以解决我的问题呢?即使父级重新渲染,单线程模拟不会阻止它并行重新渲染吗?
我想知道在设计调用 JavaScript 互操作的组件时是否应该注意竞争条件。
上面的示例是一个足够简单的修复,但我还有其他包装 JS 库的组件,这些组件具有更复杂的行为,其中 C# 和 JS 端都可以更改绑定参数,而“只是将标志移得更高”并不是如果组件可以并行重新渲染,那么它们将适用于其中一些。
文档在渲染组件之前,生命周期事件中执行的异步操作可能无法完成。这是错误的假设。异步和多线程不一样。 Blazor 的渲染是异步(交错)的,是的,当您在任何地方执行等待时,可能会发生新的渲染。这一切都发生在同一个单线程上并不那么重要。
所以你的原始代码确实有竞争条件,并且尽早设置标志确实是解决办法。
但是我还有其他封装 JS 库的组件,它们的行为要复杂得多
您必须具体情况具体分析。我认为除了不使用 AfterRender 事件来更新数据之外,没有通用的解决方案。应该可以将此逻辑移至 OnParamsSet。