在vue中,abortController调用导致的内存泄漏问题

问题代码

typescript 复制代码
<script setup lang="ts">
import { IPreviewBaseProps } from "@/components/FilePreview/interface";
interface IProps extends IPreviewBaseProps {
  abortController: AbortController;
}

const props = withDefaults(defineProps<IProps>(), {
  size: 0
});

/**
 * blob转xml
 */
const blobToXml = (blob: Blob) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = e => {
      // 拿到完整xml文本
      const xmlStr = e.target.result;
      resolve(xmlStr);
    };
    reader.onerror = error => {
      reject(error);
    };
    reader.readAsText(blob, "utf-8");
  });
};

const loading = ref(false);
const parsedXml = ref(null);
const previewXmlRef = ref(null);
const initData = async () => {
  loading.value = true;
  try {
    await nextTick();
    const start = 0;
    const end = props.size;
    const res = await props.request({ id: props.params, start, end, signal: props.abortController.signal });
    parsedXml.value = await blobToXml(res.data);
  } catch (err) {
    console.error("err", err);
  } finally {
    loading.value = false;
  }
};

onMounted(() => {
  initData();
});

onBeforeUnmount(async () => {
  // ***关键成因***
  if (props.abortController) {
    props.abortController.abort();
  }
});
</script>

<template>
  <div class="preview-text" v-loading="loading" element-loading-text="loading..."></div>
</template>

<style scoped lang="scss">
</style>

内存分析

解决方案

typescript 复制代码
onBeforeUnmount(async () => {
  // loading.value增加条件,在需要调用时候在调用
  if (loading.value && props.abortController) {
    await nextTick();
    props.abortController.abort();
  }
});

这件事到底怎么回事

可以把它想成:关抽屉时,你非要再撕一张"取消条"贴在已经办完的材料上;这张纸条上还带着"当时办公室谁在场"的名单,结果整间办公室的人都被这张纸条勾住、走不了。


1. 表面现象

XML 预览弹框关掉、点强制回收垃圾之后,内存快照里还能看到:

  • 已经从页面抠下来的 DOM(Detachedel-dialog / preview-text
  • 背后还连着 Vue 组件实例
  • 链条常经过:AbortControllerAbortSignalDOMExceptionErrorStackData → Vue 的 scope

看起来像"弹框关了东西还在",所以叫内存泄漏(至少是关不干净、回收不掉)。


2. 真正的关键角色:abort() 造出来的"错误对象"

AbortController.abort() 不只是"告诉请求停一下",浏览器还会:

  1. 新建一个 DOMException(AbortError)
  2. 挂到 signal.reason
  3. 这个错误对象自带调用栈信息 (堆里叫 ErrorStackData

调用栈可以粗暴理解成:"是谁、在哪段代码路径上喊停的"的现场记录。

在 Chrome 里,这段记录有时会顺手抓住当时还活着的 Vue 组件"内部工作区"(effect scope / 闭包),而组件又抓着自己的 DOM。

所以:

泄漏的直接推手,往往不是 XML 内容本身,而是关弹框时多造出来、又被留下来的 AbortError


3. 为什么"请求已经成功再关"也会中招?

很多人直觉会想:

请求都成功了,又没进 catch,怎么还会和 abort 有关?

因为:

  • 进不进 catch,只说明"有没有一次失败的 Promise"
  • 调不调 abort(),是另一件事

请求成功后你再关弹框,如果卸载时仍无脑:

js 复制代码
props.abortController.abort()

此时:

  • 请求早结束了 → 不会进 catch(你的观察是对的)
  • abort() 照样会新建 AbortError 并挂到 signal 上
  • 这个错误对取消请求没任何用处,却刚好够把 Vue 实例 / Detached DOM 钉住

一句话:

不是 catch 抓住了错误,而是关弹框时多余的 abort 又造了一个错误,这个错误把组件勾住了。


4. 为什么会勾住"已关闭"的弹框?(成环)

简化关系:

text 复制代码
AbortController(取消器)
    ↓
AbortError(abort 时创建,带调用栈)
    ↓
调用栈勾住 Vue 组件内部状态
    ↓
组件还能指回 AbortController(props)
    ↓
组件的 DOM(弹框内容)------页面上看不见了,但 JS 还以为有人在用

只要外面还有人握着这个 AbortController(哪怕只是一时、或工具/原生侧还挂着 signal),整圈都丢不掉 → 快照里就一直是 Detached 弹框。


5. 为什么 await nextTick() 曾经"看起来能修好"?

Vue 不会等 异步的 onBeforeUnmount 跑完再拆组件。

  • 立刻 abort:拆组件的过程中喊停 → 错误的调用栈更容易录进 Vue 正在拆的现场 → 容易钉死
  • nextTick 再 abort:组件先拆完,再喊停 → 调用栈不怎么沾那片 Vue 现场 → 看起来"好了"

所以 nextTick运气型的时间差 ,不是清晰的所有权设计。真正干净的是:没必要时不要 abort


6. 为什么 onErrorCaptured 抓不到?

那是两套系统:

AbortError 挂在 signal 上 Vue 的 onErrorCaptured
像什么 取消器上贴了张纸条 组件树报警器
要不要"抛给 Vue" 不要

abort() 主要走 Signal / 请求 Promise ,不是 Vue 组件异常冒泡。

请求失败进了你自己的 try/catch 被吞掉,Vue 更是无事可捕。

所以:堆里有 ErrorStackData,不等于组件报错了。


7. 你们最后为什么这样一改就好了?

js 复制代码
if (loading.value && props.abortController) {
  props.abortController.abort();
}

含义很直白:

  • 还在加载 (请求可能还在飞)→ 需要取消 → 才 abort()
  • 已经加载完 (你的主操作路径)→ 关弹框别 abort → 不再多余制造 AbortError → 钉链断掉

这就同时满足了:

  1. 真要中途关掉时,还能取消请求
  2. 看完再关时,不制造那场"无意义的 AbortError 挂钩"

8. 一句话总纲

造成这次问题的原因:

关 XML 弹框时(尤其在请求已成功之后)仍然调用 abort(),浏览器创建了带调用栈的 AbortError,并挂在 AbortController 上;调用栈通过 Vue 组件内部状态勾住了本应销毁的实例和 DOM,形成回收不掉的 Detached 节点。

它和"有没有进 catch"无关,本质是关抽屉时多余撕了一张会勾人的取消条

有效做法: 只在仍在 loading、确实需要取消请求时 abort;请求已成功则直接拆弹框即可。

相关推荐
运维全栈笔记3 小时前
Vue + Spring Boot 前后端分离项目部署笔记(若依 RuoYi-Vue 3.9.2)
运维·服务器·vue.js·spring boot·笔记·开源·开源软件
Simon_He14 小时前
一个渲染内核,五个框架包:我的流式 Markdown 渲染库是怎么长成“全家桶”的
前端·vue.js·markdown
夏雪coding15 小时前
JeecgBoot 动态路由踩坑(Vue2):刷新白屏、permissionList[0] 报 undefined,以及我最后为什么全删了
javascript·vue.js
前进的程序员16 小时前
Codex破局:前端组件秒级生成|提速React/Vue开发,可复用提示词+实战调试经验
前端·vue.js·react.js·codex
JunjunZ16 小时前
Vue 3 文件预览的通用设计与实现
前端·vue.js
无懈可击21 小时前
Vue 低代码可视化 AI 表单设计器 FcDesigner v3.5 版本发布!
前端·vue.js·前端框架
Web - Anonymous21 小时前
Vue3 + Element Plus 集成 JSON Editor 实现实时校验 JSON 编辑器(高亮/暗亮主题/分栏回显/组件复用) - 附完整示例
vue.js·编辑器·json
斯内普吖1 天前
(开源)农产品电商系统实战指南 基于 Java + SpringBoot + Vue + MySQL
java·vue.js·spring boot·mysql·开源
daols881 天前
vue 甘特图 vxe-gantt 紧前紧后依赖关系连接线配置详解
前端·vue.js·甘特图
剑胆琴心静水深流1 天前
全栈之路6---web集成与呈现
前端·vue.js·spring boot·分布式·spring·前端框架·npm