[英]Invoking JS Interop Function for DOM element located in submenu in Blazor
[英]Blazor component removed from DOM before disposal causes js interop to fail
我的 Blazor 组件有一些关联的 JavaScript,它执行(异步)动画。
MyComponent.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (someCondition & jsModule != null)
await jsModule.InvokeVoidAsync("startAnimation", "#my-component");
}
public async ValueTask DisposeAsync()
{
if (jsModule != null)
await jsModule.InvokeVoidAsync("stopAnimationPrematurely", "#my-component");
}
MyComponent.razor.js
export function startAnimation(id) {
let element = document.getElementById(id);
element.addEventListener(
'animationend',
function() { element.classList.remove("cool-animation") },
{ once: true }
);
element.classList.add("cool-animation");
}
export function stopAnimationPrematurely(id) {
let element = document.getElementById(id); // fails here
element.removeEventListener(
'animationend',
function() { element.classList.remove("cool-animation") },
{ once: true }
);
element.classList.remove("cool-animation");
}
如您所见, animation 会自行清理(通过{ once: true }
)。
但是,当用户单击不同的页面或组件时——因此 blazor 组件被销毁——可能会有一个 animation 正在进行中。 如果我不删除 js 事件侦听器,那么我将得到 memory 泄漏。 因此,在DisposeAsync()
中,我调用了明确调用removeEventListener()
的 js 清理代码。
问题是当 js 代码运行时,组件已经被销毁 - 因此 DOM 元素丢失,id 无效,因此 js 清理失败(并抛出)。
这是非常令人惊讶的。 各种生命周期方法和处置之间存在竞争条件。 在 MudBlazor 中,他们也遇到了这个问题,并引入了一些非常难以理解(未记录)的锁定作为解决方法。
如果没有解决方法或黑客,我该如何处理这个问题? (如果那不可能,请展示一个可行的解决方案,即使使用锁定或其他任何东西......一个 hacky 解决方案总比没有好。)
我在这里遇到了类似的问题。 然而,据我所知,一些人为的 Task.Delays 是实现竞争条件所必需的。
accepted 很有趣,但我认为它更像是一种解决方法,并没有解决核心问题。
感谢您提出这个话题并在 git 上提问。因为这看起来确实是一个很难的话题。 我发现 javiercn 的回复有助于阐明这个主题。
1.) 您正在使用“纯”JS 来引用 Blazor document.getElementById(id);
. 恕我直言,不推荐或至少不是华丽的方式,因为 Blazor不知道这些元素。 恕我直言,我会将我所有的 JS 调用拆分为“真正纯 JS”,这意味着<body>
底部的scripts
Blazor 对此一无所知,并且不会被 JS 互操作调用,例如使用普通的onclick="console.log('hi')"
qithout @
...
-要么-
将ElementReferences
与@ref
结合使用。 请参阅此处和此处。 这里需要注意的是,它们只能在OnAfterRenderAsync
中可靠地使用。
(没有在 DisposeAsync 中测试它们,在这里使用它们是否有意义?)
2.) 如果我错了,请在此处纠正我,但是当您实际向元素添加事件侦听器并且该元素从 DOM 中删除时,那么您已经很好了,这就是我所假设的。 侦听器是元素的属性并随它一起删除,这就是我的想法。 真的有 memory 泄漏吗? (这是 javiercn 在 github 上提到的“什么都不做”的方法吗?) - TBH 在这种情况下,我没有考虑必须运行清理,但你让我考虑了!
也许吧,但是,如果您在全局 object(例如window
)处设置事件侦听器,则会出现问题。 另一方面,您始终可以从 DisposeAsync 内部将其删除,因为那时该元素肯定仍然存在。
3.) 异步 LifeCycle 方法的顺序可能非常混乱,但一般来说,我会说,当一个组件在OnAfterRenderAsync
内部导航时定期处理/删除时,不会调用 OnAfterRenderAsync。但是,如果你离开一边,那就是另一回事了,比如关闭选项卡,转到空白页等。 在这里查看我的回答。
可能什么都不做,可能没问题。 在里面使用 ES6 模块和变量,与你的元素有 1:1 的相关性,至少不会导致 memory 泄漏的增加,即使仍然有一些内容(GC 可能会清理,就像 javiercn 说的那样)-(当重新创建组件时,它也会被覆盖)
变异观察者似乎是 max 最可靠的纯 JS 方法。 可靠性
尝试在 DisposeAsync 中使用 bool 标志(或锁)以防止“仍在继续” OnAfterRenderAsync(这可能是一种非常罕见的边缘情况,我在这里很关心)
基本上你的问题是渲染器已经重建它的 DOM 并将它传递给浏览器,然后它开始处理组件(现在不再附加到渲染树)并运行代码。 正如您所发现的,DOM 元素已成为历史。
因此,您需要确保 Animation 停止代码在 Render 有机会更新 DOM 之前运行。
Pre Net7.0 这是不可能的。 Net7.0 向NavigationManager
引入了一项新功能。 它现在有一个名为OnLocationChanging
的异步方法,它会在引发LocationChanged
事件之前调用该方法。 它的主要设计目的是防止导航离开脏的编辑表单。 您获取提供的LocationChangingContext
并调用PreventNavigation
以取消导航事件(没有引发LocationChanged
事件)。
您不想取消导航事件,只需延迟它直到您完成必要的家务。 它是基于任务的,因此NavigationManager
在引发LocationChanged
事件之前一直等到它完成(路由器使用此事件来触发路由,....)。 我想我们可以用它来实现你的目标。
下面是一些演示模式的代码,我相信您可以使用。
@implements IDisposable
@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
@code {
[Inject] public NavigationManager NavManager { get; set; } = default!;
private IDisposable? NavDisposer;
protected override void OnInitialized()
=> NavDisposer = NavManager.RegisterLocationChangingHandler(OnLocationChanging);
private async ValueTask OnLocationChanging(LocationChangingContext context)
=> await this.SortTheAnimation();
public async ValueTask SortTheAnimation()
{
// mock calling the js to do the cleanup and wait for the task to complete
// Make the Navigation Manager wait
await Task.Delay(3000);
}
public void Dispose()
=> this.NavDisposer?.Dispose();
}
测试一下,让我知道。
另一种方法:我也在repo 上询问过,并被告知使用MutationObserver
。
权衡:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.