8.6 KiB
8.6 KiB
紧急修复 - 变量作用域和性能警告 v3.13.5.1
📋 问题描述
1. 严重错误:ref(...) is not a function
错误信息:
batchTaskStore.js:96 Uncaught (in promise) TypeError: ref(...) is not a function
原因分析(经过两次调试):
第一次错误(行 323-325):
savedProgress变量定义在函数引用之后,导致变量访问顺序问题
第二次错误(行 96,根本原因):
- JavaScript 自动分号插入(ASI)问题
- 代码结构如下:
const savedProgress = ref(...) (() => { ... })() - JavaScript 解析器将其理解为:
const savedProgress = ref(...)((() => { ... })()) - 也就是说,解析器认为
ref()返回一个函数,然后立即调用它 - 因为
ref()返回的是一个响应式对象而不是函数,所以报错ref(...) is not a function
技术细节:
- ASI(Automatic Semicolon Insertion)是 JavaScript 的一个特性
- 当一行以
(开头时,JavaScript 可能不会自动插入分号 - 这导致前一行和当前行被连在一起解析
- 这是一个经典的 JavaScript 陷阱,特别是在使用 IIFE(立即执行函数表达式)时
2. Vue 性能警告
警告信息:
[Vue warn]: Vue received a Component that was made a reactive object.
This can lead to unnecessary performance overhead and should be avoided
by marking the component with `markRaw` or using `shallowRef` instead of `ref`.
原因分析:
Home.vue中的featureCards和features数组包含了组件对象(PersonCircle、Cube、Ribbon、Settings)- 这些组件被 Vue 的响应式系统包装,导致不必要的性能开销
- 组件本身不需要响应式追踪,应该使用
markRaw标记
🔧 修复方案
修复 1:调整变量定义顺序 + 修复 ASI 问题
文件: src/stores/batchTaskStore.js
修改内容:
- 将
savedProgress的定义从第 323 行移到第 93 行(在被引用的函数之前) - 将初始化 IIFE 也一起移动,确保在 store 初始化早期就完成进度数据的清理
- 关键修复: 在
ref()调用和 IIFE 之间添加分号,避免 ASI 问题
修改前(第 322-344 行):
// 任务执行历史记录(最近10次)
const executionHistory = ref(...)
// 批量任务执行进度记录(用于刷新后恢复)
const savedProgress = ref(
JSON.parse(localStorage.getItem('batchTaskProgress') || 'null')
)
// 🆕 v3.13.5: 启动时清理过期的进度数据
(() => {
if (savedProgress.value) {
// ...清理逻辑
}
})()
修改后(第 93-115 行):
// 🆕 连接池实例
let wsPool = null
// 批量任务执行进度记录(用于刷新后恢复)
const savedProgress = ref(
JSON.parse(localStorage.getItem('batchTaskProgress') || 'null')
); // ⚠️ 关键:添加分号,避免 ASI 问题
// 🆕 v3.13.5: 启动时清理过期的进度数据
(() => {
if (savedProgress.value) {
const progress = savedProgress.value
const now = Date.now()
const elapsed = now - (progress.timestamp || 0)
const isExpired = elapsed > 24 * 60 * 60 * 1000 // 24小时
if (isExpired) {
console.log(`🧹 [启动清理] 清除过期的进度数据 (${Math.floor(elapsed / 3600000)} 小时前)`)
localStorage.removeItem('batchTaskProgress')
savedProgress.value = null
} else if (progress.completedTokenIds && progress.allTokenIds) {
const remaining = progress.allTokenIds.length - progress.completedTokenIds.length
console.log(`📂 [启动恢复] 发现未完成的进度数据: ${progress.completedTokenIds.length}/${progress.allTokenIds.length} (剩余 ${remaining} 个)`)
}
}
})(); // ⚠️ 也添加分号,保持一致性
// 日志包装函数:根据开关决定是否输出
const batchLog = (...args) => {
if (ENABLE_BATCH_LOGS) {
console.log(...args)
}
}
效果:
- ✅
savedProgress在被函数引用前就已定义 - ✅ 添加分号明确语句边界,避免 ASI 陷阱
- ✅ 防止
ref()的返回值被误认为是函数并调用 - ✅ 确保 store 初始化过程中的正确执行顺序
修复 2:使用 markRaw 标记组件
文件: src/views/Home.vue
修改内容:
- 导入
markRaw函数 - 使用
markRaw()包装所有组件对象
修改前:
import { ref, onMounted } from 'vue'
// ...
const featureCards = ref([
{
id: 1,
icon: PersonCircle, // 未标记,会被包装为响应式
title: '角色管理',
description: '统一管理游戏角色'
},
// ...
])
修改后:
import { ref, markRaw, onMounted } from 'vue'
// ...
const featureCards = ref([
{
id: 1,
icon: markRaw(PersonCircle), // 使用 markRaw 标记,不会被响应式包装
title: '角色管理',
description: '统一管理游戏角色'
},
// ...
])
应用范围:
featureCards数组中的 3 个组件:PersonCircle, Cube, Ribbonfeatures数组中的 4 个组件:PersonCircle, Cube, Ribbon, Settings
效果:
- ✅ 消除 Vue 性能警告
- ✅ 减少不必要的响应式追踪开销
- ✅ 提升渲染性能
📊 影响范围
文件修改
-
src/stores/batchTaskStore.js
- 变量定义位置调整
- 无功能变更,只是顺序优化
-
src/views/Home.vue
- 添加
markRaw标记 - 无功能变更,性能优化
- 添加
用户影响
- ✅ 修复了启动时的严重错误
- ✅ 消除了控制台警告
- ✅ 提升了页面渲染性能
- ✅ 不影响任何现有功能
🧪 测试建议
1. 基本功能测试
- 启动应用,确认没有错误信息
- 访问首页,确认无 Vue 警告
- 检查控制台,确认无
ref(...) is not a function错误
2. 批量任务测试
- 执行批量任务,任务中断后刷新页面
- 确认进度恢复功能正常
- 检查进度数据过期清理是否正常工作
3. 性能观察
- 观察首页渲染速度
- 检查浏览器内存占用
- 确认组件图标正常显示
📝 技术说明
markRaw 的作用
markRaw() 是 Vue 3 提供的工具函数,用于标记一个对象,使其永远不会被转换为响应式对象。
适用场景:
- 组件定义对象(如本次修复)
- 大型不可变数据结构
- 第三方库实例
- DOM 元素引用
优势:
- 减少响应式系统的追踪开销
- 避免不必要的深度响应式转换
- 提升性能,特别是在处理大量数据时
JavaScript ASI(自动分号插入)陷阱
这是一个经典的 JavaScript 陷阱,值得所有开发者注意:
问题场景:
当一行以 (, [, `, +, -, / 等符号开头时,JavaScript 可能不会在上一行末尾自动插入分号。
危险示例:
// ❌ 错误:会被解析为 fn1()(fn2())
const result = fn1()
(fn2())
// ❌ 错误:会被解析为 arr[0]
const arr = [1, 2, 3]
[0].forEach(...)
// ✅ 正确:显式添加分号
const result = fn1();
(fn2())
// ✅ 正确:或使用分号开头(防御性编程)
const result = fn1()
;(fn2())
在本项目中的体现:
// ❌ 会被解析为: ref(...)((() => {})())
const savedProgress = ref(...)
(() => {})()
// ✅ 正确:添加分号
const savedProgress = ref(...);
(() => {})()
最佳实践:
- 使用一致的分号风格:要么总是加,要么总是不加(配合 linter)
- IIFE 前加分号:如果不用分号风格,在 IIFE 前加
;保护 - 使用 ESLint:配置
semi规则强制统一风格 - 代码审查:特别注意以
(或[开头的行
变量定义顺序的重要性
在 Pinia setup store 中,虽然闭包允许函数引用后面定义的变量,但:
computed属性可能在定义时就触发计算- IIFE(立即执行函数)会在定义时立即执行
- 某些情况下可能导致访问未初始化的变量
最佳实践:
- 将被多处引用的状态变量定义在前面
- 确保在使用前完成初始化
- 特别注意
computed和 IIFE 的执行时机 - 在
ref()等返回对象的调用后,如果紧跟 IIFE,必须加分号
🎯 版本信息
- 版本号: v3.13.5.1
- 修复类型: 紧急修复
- 优先级: 🔴 高(阻塞性错误)
- 向后兼容: ✅ 完全兼容