Blazor中OnInitializedAsync用await时OnAfterRenderAsync提前触发致JS报错
在Blazor Server页面中,重写OnAfterRenderAsync调用JavaScript时遇到异常:当OnInitializedAsync中使用带await的异步操作(比如示例中的await Task.Delay(1000)),会抛出以下错误:
blazor.server.js:1 [2023-09-14T15:14:51.292Z] Error: Microsoft.JSInterop.JSException: Cannot read properties of null (reading 'value')
若去掉异步等待逻辑,调用非异步版本则正常,且全程已正确使用await。即使在代码中添加了Initialised和Rendered状态检查,确保两个生命周期方法都完成后再执行JS,问题依然存在。
复现示例代码
可替换默认模板的FetchData.razor页面测试:
@page "/fetchdata" @inject IJSRuntime JS @LogSteps <br /> @if (tmpExample == null) { <p><em>Loading...</em></p> } else { <EditForm Model="@tmpExample"> <InputNumber id="IntMyProperty" @bind-Value="tmpExample.IntMyProperty" /> <InputText id="StringProperty" @bind-Value="tmpExample.StringProperty" /> </EditForm> } @code { private bool Initialised = false; private bool Rendered = false; protected override async Task OnInitializedAsync() { LogStep("OnInitializedAsync-Start;"); await Task.Delay(1000); // 注释掉该行就能正常运行! tmpExample = new() { IntMyProperty = 1, StringProperty = "hello" }; LogStep("OnInitializedAsync-End;"); Initialised = true; await InvokeJavaScript(); } public string LogSteps = ""; void LogStep(string step) { LogSteps += step; Console.WriteLine(step); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { LogStep("OnAfterRenderAsync-FirstRender;"); Rendered = true; await InvokeJavaScript(); } else { LogStep("OnAfterRenderAsync-OtherRender;"); } } internal Example tmpExample; internal class Example { public int IntMyProperty { get; set; } public string StringProperty { get; set; } } async Task InvokeJavaScript() { LogStep("InvokeJavaScript-1-Initialised=" + Initialised.ToString() + "; Rendered=" + Rendered.ToString() + ";"); if (Initialised && Rendered) { LogStep("InvokeJavaScript-StartingRun;"); await JS.InvokeVoidAsync("eval", "alert(document.getElementById('StringProperty').value)"); LogStep("InvokeJavaScript-FinishedRun;"); } } }
问题核心原因
当OnInitializedAsync存在异步延迟时,Blazor的渲染流程会先完成第一次OnAfterRenderAsync(此时tmpExample为null,页面仅显示Loading,表单元素尚未渲染)。之后OnInitializedAsync完成,设置tmpExample并触发重新渲染,但这次渲染的OnAfterRenderAsync中firstRender为false,不会更新Rendered状态。此时虽然Initialised和Rendered都为true,但实际表单元素是在第二次渲染才生成的,第一次渲染的DOM中并没有目标元素,JS调用基于的是旧DOM状态,因此报错。
解决思路与方案
方案1:调整JS调用时机到元素真正渲染完成后
移除Rendered状态变量,改为在OnAfterRenderAsync中检查tmpExample是否已初始化,确保表单元素已渲染后再执行JS,同时移除OnInitializedAsync中的JS调用:
protected override async Task OnInitializedAsync() { LogStep("OnInitializedAsync-Start;"); await Task.Delay(1000); tmpExample = new() { IntMyProperty = 1, StringProperty = "hello" }; LogStep("OnInitializedAsync-End;"); Initialised = true; // 移除这里的InvokeJavaScript调用,此时DOM还未更新 } protected override async Task OnAfterRenderAsync(bool firstRender) { // 只要tmpExample存在,说明表单元素已渲染 if (tmpExample != null) { LogStep("OnAfterRenderAsync-TryingJS;"); await JS.InvokeVoidAsync("eval", "alert(document.getElementById('StringProperty').value)"); } }
方案2:使用ElementReference代替ID选择器(推荐)
Blazor官方推荐使用ElementReference直接引用DOM元素,避免ID选择器的不确定性:
- 修改Razor部分的InputText:
<InputText @ref="stringInputRef" @bind-Value="tmpExample.StringProperty" />
- 在@code块中定义元素引用:
private ElementReference stringInputRef;
- 在
OnAfterRenderAsync中调用JS时使用该引用:
protected override async Task OnAfterRenderAsync(bool firstRender) { if (tmpExample != null) { // 调用自定义JS函数,传入元素引用 await JS.InvokeVoidAsync("alertElementValue", stringInputRef); } }
- 在_Host.cshtml中添加对应的JS函数:
window.alertElementValue = function(element) { alert(element.value); }
方案3:优化状态检查与重试逻辑
如果需要保留原有状态检查逻辑,可修改Rendered更新时机,并在每次渲染时尝试执行JS,同时检查元素是否存在:
protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { LogStep("OnAfterRenderAsync-FirstRender;"); Rendered = true; } else { LogStep("OnAfterRenderAsync-OtherRender;"); } // 每次渲染都尝试执行JS await InvokeJavaScript(); } async Task InvokeJavaScript() { LogStep("InvokeJavaScript-1-Initialised=" + Initialised.ToString() + "; Rendered=" + Rendered.ToString() + ";"); if (Initialised && Rendered) { // 先检查元素是否存在 var elementExists = await JS.InvokeAsync<bool>("eval", "document.getElementById('StringProperty') !== null"); if (elementExists) { LogStep("InvokeJavaScript-StartingRun;"); await JS.InvokeVoidAsync("eval", "alert(document.getElementById('StringProperty').value)"); LogStep("InvokeJavaScript-FinishedRun;"); } else { LogStep("InvokeJavaScript-ElementNotFound;"); } } }
内容的提问来源于stack exchange,提问作者Jay

