Vitest detectAsyncLeaks 配置详解:用 `node:async_hooks` 精准定位异步资源泄漏 📅 发布时间:2026/9/14 1:59:46 👁 浏览次数: Vitest detectAsyncLeaks 配置详解用node:async_hooks精准定位异步资源泄漏【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest导读detectAsyncLeaks是 Vitest 提供的一项测试诊断配置开启后Vitest 会借助 Node.js 内置的node:async_hooks跟踪测试文件中创建的异步资源若某个资源在测试结束后仍未清理便会在测试收尾时以醒目格式打印泄漏报告。本文围绕 detectasyncleaks.md 展开先讲清配置方式、运行机制与典型泄漏案例再从 detect-async-leaks.ts 源码出发剖析其实现细节并给出可复用的清理修复方案与多场景实测输出帮助你把它作为调试利器而不是默认开启项。配置项速览项目值类型booleanCLI 开关--detectAsyncLeaks等价写法--detect-async-leaks默认值false关闭配置方式有三种效果完全等价1. 配置文件vitest.config.tsimport { defineConfig } from vitest/config export default defineConfig({ test: { detectAsyncLeaks: true, }, })2. CLI 传参vitest --detectAsyncLeaks # 或使用连字符风格 vitest --detect-async-leaks3. 仅本次运行时临时开启推荐vitest run --detectAsyncLeaks该 CLI 选项在 cli-config.ts 中注册其描述为 Detect asynchronous resources leaking from the test file (default:false)并在 resolveProjects.ts 中被列为 project 级可透传配置。::: warning 性能警告开启该选项会让测试运行明显变慢官方文档明确提示仅在调试或开发测试时使用不要在 CI 或日常跑测中常开。 :::之所以慢是因为每个测试文件都要额外挂载一个async_hooks钩子、为每次异步资源创建抓取堆栈、并在收尾阶段等待事件循环空转后再汇总扫描详见下文实现原理。此外配置解析在 resolveConfig.ts 中还有一个重要限制浏览器模式browser.enabled下该选项不受支持Vitest 会打印黄色警告 The optiondetectAsyncLeaksis not supported in browser mode and will be ignored. 并直接忽略它——也就是说它只对 Node 环境下的测试生效。它到底在检测什么detectAsyncLeaks的职责是检测从测试文件中泄漏出去的异步资源。这里的异步资源指 Node.js 中一切由async_hooks追踪的对象典型代表包括Timeout—— 未被清除的setTimeout/setInterval定时器PROMISE—— 永不 settle 的 Promise例如new Promise(() {})FSEVENTWRAP—— 未关闭的fs.watch文件系统监视器TCPSERVERWRAP—— 未关闭的 HTTP/TCP 服务器其他如HTTPCLIENTREQUEST、SIGNALWRAP、WRITEWRAP等各类底层资源实现上Vitest 调用node:async_hooks的createHook来追踪资源的创建init与销毁destroy。如果一个资源在测试结束后仍然存活active它就会被记录为泄漏。从源码看检测流程核心实现在 detect-async-leaks.ts其工作流程可拆解为四个阶段挂载钩子createHook注册init/destroy/promiseResolve三个回调随后hook.enable()开始全局追踪记录资源与堆栈init回调中对每个新建异步资源记录其asyncId、type和创建时刻的调用堆栈通过构造new Error(VITEST_DETECT_ASYNC_LEAKS)抓取并把堆栈与测试文件关联起来判断存活若资源对象实现了hasRef()定时器、socket 等用WeakRef包裹并在收集阶段调用ref.deref()?.hasRef()判断是否仍被引用否则默认视为存活isActiveDefault恒为true收尾收集返回一个collect()闭包先await new Promise(resolve setImmediate(resolve))让当前事件循环轮转一次再hook.disable()停止追踪最后遍历resources表把所有仍存活的资源包装成AsyncLeak含filename、projectName、stack、type四个字段见 general.ts返回。值得注意的两个实现细节忽略白名单源码第 8-19 行维护了IGNORED_TYPES集合包括DNSCHANNEL、TCPWRAP、TIMERWRAP、TLSWRAP、ZLIB等类型——这些是 Node 内部高频出现、不代表业务泄漏的资源直接跳过以降低噪音堆栈归属判定init回调里如果当前堆栈不包含测试文件路径则尝试通过triggerAsyncId回溯到触发它的父资源resources.get(triggerAsyncId)从而把间接由测试代码创建的资源也正确归因到测试文件上。在测试运行中的挂载位置detectAsyncLeaks的开启与收集发生在 worker 内部。以 runBaseTests.ts 为例const collectAsyncLeaks config.detectAsyncLeaks ? detectAsyncLeaks(file.filepath, workerState.ctx.projectName) : undefined await startTests([file], testRunner) // 执行测试文件 const leaks await collectAsyncLeaks?.() // 测试结束后收集泄漏 if (leaks?.length) { workerState.rpc.onAsyncLeaks(leaks) // 通过 RPC 上报给主进程 }每个测试文件都会独立创建/收集一次钩子因此泄漏报告能精确到文件。同一逻辑在 runVmTests.ts 中用于 VM 模式pool: vmThreads。收集到的泄漏列表经 RPC 上报主进程后由 rpc.ts 的onAsyncLeaks回调交给vitest.state.catchLeaks见 state.ts统一收进leakSet最终由默认 reporter 渲染成 Async Leaks 错误块输出。泄漏长什么样错误输出解读开启后如果代码里有在测试结束后才触发的异步回调你会看到如下格式的错误以setTimeout为例来自官方文档⎯⎯⎯⎯⎯⎯⎯⎯ Async Leaks 1 ⎯⎯⎯⎯⎯⎯⎯⎯ Timeout leaking in test/checkout-screen.test.tsx 26| 27| useEffect(() { 28| setTimeout(() setWindowWidth(window.innerWidth), 150) 29| | ^ 30| }) 31|输出解读标题行Async Leaks N汇总泄漏总数首行Timeout leaking in 测试文件路径点明泄漏资源类型Timeout与所属文件紧随其后的是资源创建处的源代码片段与行号^指向创建调用位置方便你快速定位问题代码。真实泄漏场景与实测输出仓库的 e2e 测试 detect-async-leaks.test.ts 覆盖了定时器、Promise、fetch、fs watcher、HTTP server、多项目等场景。下面按资源类型整理实测结果均以globals: true, detectAsyncLeaks: true, pool: forks运行。1. 定时器Timeout / Timeout 变体test(leak in test file, () { setTimeout(() {}, 100_000) }) test(not a leak, () { const timeout setTimeout(() {}, 100_000) clearTimeout(timeout) })输出节选第一段泄漏来自测试文件内的直接调用⎯⎯⎯⎯⎯⎯⎯ Async Leaks 2 ⎯⎯⎯⎯⎯⎯⎯⎯ Timeout leaking in packages/example/test/example.test.ts 5| setTimeout(() {}, 100_000) | ^如果泄漏发生在被测试文件导入的源码模块里输出会带调用链先在src/source.ts中标出资源创建位置再通过❯箭头回溯到测试文件的调用行❯ execute packages/example/src/source.ts:3:9 ❯ packages/example/test/example.test.ts:9:9这正是前面提到的triggerAsyncId归因逻辑在工作——资源虽在模块内创建但能被正确归因到发起调用的测试文件。2. Promisetest(leak, () { new Promise((resolve) {}) }) test(not a leak, () { new Promise((resolve) resolve()) new Promise((resolve, reject) reject()).catch(() {}) })输出显示PROMISE leaking且嵌套 Promise 会分别报告外层 Promise 指向外层new Promise内层 Promise 指向内层new Promise并附带❯链说明其触发上下文。而已经 resolve / reject 的 Promise 因为走了promiseResolve回调被从资源表中移除不会误报。3. fs.watch 文件监视器import { watch } from node:fs test(leaking fs watcher, () { watch(import.meta.filename, () {}) // 泄漏 }) // 修复watch(...).close()输出为FSEVENTWRAP leaking这是 Node 底层文件系统事件包装资源调用.close()后即不再报告。4. HTTP 服务器const app new Server() await new Promise(resolve app.listen({ host: localhost, port: 0 }, resolve)) // 泄漏缺少 app.close()输出为TCPSERVERWRAP leaking对应地测试末尾执行app.close()后不再报告。5. 关闭开关时不报告同一份到处泄漏的测试代码在detectAsyncLeaks: false下运行时stdout 不含任何 Leak 字样且 stderr 为空见测试文件首个用例证明该功能默认关闭、零副作用。6. 多项目workspace场景projects: [packages/*]下运行两个子项目泄漏报告会分别标注各自的项目名与文件路径⎯⎯⎯⎯⎯⎯⎯ Async Leaks 2 ⎯⎯⎯⎯⎯⎯⎯⎯ Timeout leaking in packages/first/test/example-1.test.ts Timeout leaking in packages/second/test/example-2.test.tsprojectName字段由 worker 传入detectAsyncLeaks(testFile, projectName)见 runBaseTests.ts在多项目下依然能精确归属。如何修复泄漏清理资源的正确姿势官方文档以 ReactuseEffect中的定时器为例给出修复前后对比useEffect(() { setTimeout(setWindowWidth, 150, window.innerWidth) // 修复前定时器无人引用必然泄漏 const timeout setTimeout(setWindowWidth, 150, window.innerWidth) // 修复后保存引用 return function cleanup() { // 修复后卸载时清理 clearTimeout(timeout) } })修复要点可总结为一条通用原则凡是创建了异步资源的代码都必须保存其句柄并在合适的时机主动销毁资源创建 API清理 API一次性定时器setTimeoutclearTimeout(handle)周期定时器setIntervalclearInterval(handle)Promisenew Promise(...)确保 executor 一定会调用resolve/reject文件监视器fs.watch(...)watcher.close()HTTP 服务器server.listen(...)server.close()HTTP 请求fetch(...)借助AbortController中断监听器/订阅emitter.on(...)/addEventListeneremitter.off(...)/removeEventListener落实到测试代码里最稳妥的清理时机是afterEach/afterAll或在测试内同步收尾import { afterEach, test } from vitest afterEach(() { // 统一清理确保每个用例结束后无残留定时器 }) test(setInterval 用完必须清理, () { const interval setInterval(() {}, 100_000) // ...业务断言... clearInterval(interval) // 测试内直接收尾 })注意一个细节即便定时器用了.unref()如 e2e 测试中setTimeout(...).unref()的 HTTP 响应场景只要资源在测试结束时仍 active 就仍会被报告——detectAsyncLeaks关注的是资源是否存活而不是它是否阻止进程退出。什么时候该用它使用建议推荐场景调试测试为何跑不完或 hang 住泄漏的定时器/服务器/监视器是测试卡死的高频元凶开发阶段自查新写的组件尤其带useEffect、setInterval的 UI 代码或工具库创建服务器、watcher提交前临时开启跑一遍排查 CI 上偶发的超时与资源耗尽用vitest run --detectAsyncLeaks单次验证。不建议常开日常开发 watch 模式async_hooks全量追踪 每次资源创建抓堆栈开销显著官方明确警告会让测试much slowerCI 全量运行除非泄漏已造成实际问题否则慢速代价不划算。最佳实践平时保持默认关闭仅在需要时以 CLI 参数临时开启定位到泄漏并修复后立即关闭让该选项回归调试专用定位。小结detectAsyncLeaks是 Vitest 针对异步资源泄漏提供的一把精准手术刀配置一行即可开启输出直接定位到资源创建处的源码行配合node:async_hooks的底层追踪能快速揪出定时器、Promise、文件监视器、HTTP 服务器等各类未清理资源。它牺牲的性能换来的是调试阶段的确定性——记得只在调试时使用它。相关资源官方配置文档docs/config/detectasyncleaks.md核心实现源码packages/vitest/src/runtime/detect-async-leaks.ts运行时挂载逻辑packages/vitest/src/runtime/runBaseTests.tsCLI 选项注册packages/vitest/src/node/cli/cli-config.ts配置解析与浏览器模式限制packages/vitest/src/node/config/resolveConfig.ts泄漏汇总与上报packages/vitest/src/node/state.ts、packages/vitest/src/node/pools/rpc.tse2e 测试用例test/e2e/test/detect-async-leaks.test.ts【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考