在处理之前从DOM中删除的Blazor组件导致js-interop失败



My 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");
}

正如您所看到的,动画会在自身之后进行清理(通过{ once: true })。

然而,当用户单击另一个页面或组件时——因此blazor组件被破坏——可能会有一个动画正在进行中。如果我不删除js事件侦听器,那么我将出现内存泄漏。因此,在DisposeAsync()中,我调用js清理代码,该代码显式调用removeEventListener()

问题是,当js代码运行时,组件已经被破坏——因此DOM元素丢失,id无效,因此js清理失败(并抛出)。

这是非常令人惊讶的。各种生命周期方法和处置之间存在竞争条件。在MudBlazor中,他们也遇到了这种情况,并引入了一些非常难以理解的(未记录的)锁定作为解决方法。

如果没有变通方法或破解方法,我该如何处理(如果这不可能,请展示一个有效的解决方案,即使使用锁定或其他什么……一个破解的解决方案总比什么都没有好。)

基本上,您的问题是渲染器在处理组件(现在不再连接到渲染树)并运行代码之前,已经重建了它的DOM并将其传递给浏览器。正如您所发现的,DOM元素是历史。

因此,您需要确保在Render有机会更新DOM之前运行动画停止代码。

在Net7.0之前,这是不可能的。Net7.0为NavigationManager引入了一项新功能。它现在有一个名为OnLocationChanging的异步方法,在引发LocationChanged事件之前调用该方法。它主要用于防止导航离开脏的Edit Form。您使用提供的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();
}

测试一下,让我知道。

另一种方法:我还询问了回购,被告知使用MutationObserver

权衡:

  • 接受的答案是一个更轻松的选择。但只有当页面URL发生更改时,它才适用——当只删除组件时(在这种情况下没有URL更改),它就不起作用
  • MutationObserver感觉更重,但它总是可以工作的——它不依赖于URL的更改

此外,我在repo上添加了一个请求,为我们提供了一种在blazor到js-interop中处理处置的托管方式。如果你需要这个功能,请投赞成票。

更新

有人批评回购问题中的各种方法。看起来MutationObserver无疑是最好的方法,尽管需要做更多的工作。

我在这里遇到了类似的问题。然而,有些人为的任务。就我记忆所及,为了达到比赛状态,延误是必要的。

接受是有趣的,但我认为这更像是一种变通方法,并没有解决核心问题。

感谢您提出这个话题并询问git。因为这确实是一个看起来很难的主题。我发现javiercn的回答有助于阐明这个问题。

需要考虑的几件事:

1.)您正在使用";纯";JS引用Blazordocument.getElementById(id);中的元素。IMHO,这是不推荐的,或者至少不是blazor ry的方式,因为blazor不知道这些元素。IMHO我会把我所有的JS调用分成两个";真正纯JS";,这意味着位于<body>底部的scripts,Blazor对此一无所知,也不被JS interop调用,例如使用普通的onclick="console.log('hi')"qithout the@。。。

-或-

使用CCD_ 21和CCD_。看看这里和这里。这里需要注意的是,它们只能在OnAfterRenderAsync中可靠地使用。

(还没有在DisposeSync中测试过它们,在这里使用它们有意义吗?)

2.)如果我错了,请在这里纠正我,但当你实际上向一个元素添加了一个事件侦听器,并且该元素从DOM中删除时,那么你就已经很好了,这就是我所假设的。监听器是元素的一个属性,并随它一起移除,这就是我所想的。真的有内存泄漏吗?(这就是javiercn在github上提到的"什么都不做"的方法吗?)-TBH在这种情况下,我没有考虑过必须进行清理,但你让我思考了这一点!

也许,但是,如果在全局对象(如window)上有事件侦听器,则会出现问题。另一方面,您总是可以将其从DisposeSync中删除,因为那时这些元素肯定仍然存在。

3.)Async LifeCycle方法的顺序可能非常令人困惑,但通常情况下,我想说,当组件在Blazor内部导航时被定期处理/移除时,OnAfterRenderAsync不会被调用。然而,如果你把放在一边,那就另当别论了,比如关闭选项卡,转到空白页等等。请在此处查看我的答案。

总结

  • 可能什么都不做,也可以。在里面使用ES6模块和变量,这些模块和变量与元素有1:1的相关性,至少不会导致内存泄漏,即使仍然有一些内容(GC可能会清理,就像javiercn所说的)-(当重新创建组件时,这些内容也会被覆盖)

  • 突变观测器似乎是实现最大可靠性的最可靠的纯JS方法

  • 尝试在DisposeSync中使用bool标志(或锁)来防止"停止">OnAfterRenderAsync(这可能是一种非常罕见的边缘情况,我在这里担心)

最新更新