繁体   English   中英

Blazor 组件在处置前从 DOM 中移除导致 js 互操作失败

[英]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

权衡:

  • 接受的答案是一个较轻的选项。 但它仅在页面 URL 发生变化时才适用 - 当一个人只是删除组件时它不会起作用(在这种情况下没有 URL 变化)。
  • MutationObserver 感觉更重,但它始终有效——它不依赖于 URL 更改。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM