You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor组件销毁前被移除出DOM导致JS互操作失败

Blazor组件异步动画销毁时的内存泄漏与DOM竞态问题解决

问题背景

Blazor组件关联异步动画的JavaScript代码,动画通过{ once: true }自动清理事件监听器,但当用户切换页面或销毁组件时,若动画仍在运行,必须显式清理监听器避免内存泄漏。当前在DisposeAsync()中调用JS清理代码时,存在两个核心问题:

  1. 组件销毁后DOM元素已被移除,JS通过ID查找元素失败并抛出异常
  2. removeEventListener无效,因为每次添加的都是匿名函数,引用无法匹配

现有代码

MyComponent.razor

@implements IAsyncDisposable
@inject IJSRuntime JSRuntime

<div id="my-component">...</div>

@code {
  private IJSObjectReference? jsModule;
  private bool someCondition = true; // 示例条件

  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");
      await jsModule.DisposeAsync();
    }
  }
}

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);      // 此处执行失败
  element.removeEventListener(
    'animationend',
    function() { element.classList.remove("cool-animation") },
    { once: true }
  );
  element.classList.remove("cool-animation");
}

解决方案

方案1:缓存事件处理函数 + 容错DOM查找

修改JS代码,用Map缓存每个元素对应的事件处理函数,避免匿名函数导致的removeEventListener失效,同时增加元素存在性检查,防止DOM已销毁时报错。

// 缓存元素ID与对应动画结束处理函数的映射
const animationHandlers = new Map();

export function startAnimation(id) {
  const element = document.getElementById(id);
  if (!element) return;

  // 定义可复用的处理函数
  const handler = () => {
    element.classList.remove("cool-animation");
    animationHandlers.delete(id);
  };

  element.addEventListener('animationend', handler, { once: true });
  element.classList.add("cool-animation");
  animationHandlers.set(id, handler);
}

export function stopAnimationPrematurely(id) {
  const handler = animationHandlers.get(id);
  if (!handler) return;

  const element = document.getElementById(id);
  if (element) {
    element.removeEventListener('animationend', handler, { once: true });
    element.classList.remove("cool-animation");
  }

  animationHandlers.delete(id);
}

Blazor组件代码无需修改,此方案兼容原有ID调用方式,同时解决了元素不存在报错和监听器无法移除的问题。

方案2:使用ElementReference替代ID(推荐)

直接使用Blazor的ElementReference传递DOM元素引用,避免ID查找的竞态问题,同时简化JS逻辑。

修改Blazor组件

@implements IAsyncDisposable
@inject IJSRuntime JSRuntime

<div @ref="componentElement">...</div>

@code {
  private ElementReference componentElement;
  private IJSObjectReference? jsModule;
  private bool someCondition = true;

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    if (someCondition)
    {
      jsModule ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./MyComponent.razor.js");
      await jsModule.InvokeVoidAsync("startAnimation", componentElement);
    }
  }

  public async ValueTask DisposeAsync()
  {
    if (jsModule != null)
    {
      await jsModule.InvokeVoidAsync("stopAnimationPrematurely", componentElement);
      await jsModule.DisposeAsync();
    }
  }
}

修改JS代码

// 缓存元素引用与对应处理函数的映射,WeakMap会在元素销毁时自动回收
const animationHandlers = new WeakMap();

export function startAnimation(element) {
  if (!element) return;

  const handler = () => {
    element.classList.remove("cool-animation");
    animationHandlers.delete(element);
  };

  element.addEventListener('animationend', handler, { once: true });
  element.classList.add("cool-animation");
  animationHandlers.set(element, handler);
}

export function stopAnimationPrematurely(element) {
  const handler = animationHandlers.get(element);
  if (!handler) return;

  try {
    element.removeEventListener('animationend', handler, { once: true });
    element.classList.remove("cool-animation");
  } catch (e) {
    // 元素已被DOM移除,忽略错误
  }

  animationHandlers.delete(element);
}

此方案优势:

  • 避免ID冲突和查找失败的问题
  • 使用WeakMap缓存,元素被销毁时会自动回收,进一步降低内存泄漏风险
  • 直接操作元素引用,逻辑更简洁

内容的提问来源于stack exchange,提问作者lonix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 14:45:55