282 lines
8.6 KiB
Markdown
282 lines
8.6 KiB
Markdown
# 紧急修复 - 变量作用域和性能警告 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)问题**
|
||
- 代码结构如下:
|
||
```javascript
|
||
const savedProgress = ref(...)
|
||
|
||
(() => { ... })()
|
||
```
|
||
- JavaScript 解析器将其理解为:
|
||
```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`
|
||
|
||
**修改内容:**
|
||
1. 将 `savedProgress` 的定义从第 323 行移到第 93 行(在被引用的函数之前)
|
||
2. 将初始化 IIFE 也一起移动,确保在 store 初始化早期就完成进度数据的清理
|
||
3. **关键修复:** 在 `ref()` 调用和 IIFE 之间添加分号,避免 ASI 问题
|
||
|
||
**修改前(第 322-344 行):**
|
||
```javascript
|
||
// 任务执行历史记录(最近10次)
|
||
const executionHistory = ref(...)
|
||
|
||
// 批量任务执行进度记录(用于刷新后恢复)
|
||
const savedProgress = ref(
|
||
JSON.parse(localStorage.getItem('batchTaskProgress') || 'null')
|
||
)
|
||
|
||
// 🆕 v3.13.5: 启动时清理过期的进度数据
|
||
(() => {
|
||
if (savedProgress.value) {
|
||
// ...清理逻辑
|
||
}
|
||
})()
|
||
```
|
||
|
||
**修改后(第 93-115 行):**
|
||
```javascript
|
||
// 🆕 连接池实例
|
||
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`
|
||
|
||
**修改内容:**
|
||
1. 导入 `markRaw` 函数
|
||
2. 使用 `markRaw()` 包装所有组件对象
|
||
|
||
**修改前:**
|
||
```javascript
|
||
import { ref, onMounted } from 'vue'
|
||
// ...
|
||
const featureCards = ref([
|
||
{
|
||
id: 1,
|
||
icon: PersonCircle, // 未标记,会被包装为响应式
|
||
title: '角色管理',
|
||
description: '统一管理游戏角色'
|
||
},
|
||
// ...
|
||
])
|
||
```
|
||
|
||
**修改后:**
|
||
```javascript
|
||
import { ref, markRaw, onMounted } from 'vue'
|
||
// ...
|
||
const featureCards = ref([
|
||
{
|
||
id: 1,
|
||
icon: markRaw(PersonCircle), // 使用 markRaw 标记,不会被响应式包装
|
||
title: '角色管理',
|
||
description: '统一管理游戏角色'
|
||
},
|
||
// ...
|
||
])
|
||
```
|
||
|
||
**应用范围:**
|
||
- `featureCards` 数组中的 3 个组件:PersonCircle, Cube, Ribbon
|
||
- `features` 数组中的 4 个组件:PersonCircle, Cube, Ribbon, Settings
|
||
|
||
**效果:**
|
||
- ✅ 消除 Vue 性能警告
|
||
- ✅ 减少不必要的响应式追踪开销
|
||
- ✅ 提升渲染性能
|
||
|
||
## 📊 影响范围
|
||
|
||
### 文件修改
|
||
1. **src/stores/batchTaskStore.js**
|
||
- 变量定义位置调整
|
||
- 无功能变更,只是顺序优化
|
||
|
||
2. **src/views/Home.vue**
|
||
- 添加 `markRaw` 标记
|
||
- 无功能变更,性能优化
|
||
|
||
### 用户影响
|
||
- ✅ 修复了启动时的严重错误
|
||
- ✅ 消除了控制台警告
|
||
- ✅ 提升了页面渲染性能
|
||
- ✅ 不影响任何现有功能
|
||
|
||
## 🧪 测试建议
|
||
|
||
### 1. 基本功能测试
|
||
- [ ] 启动应用,确认没有错误信息
|
||
- [ ] 访问首页,确认无 Vue 警告
|
||
- [ ] 检查控制台,确认无 `ref(...) is not a function` 错误
|
||
|
||
### 2. 批量任务测试
|
||
- [ ] 执行批量任务,任务中断后刷新页面
|
||
- [ ] 确认进度恢复功能正常
|
||
- [ ] 检查进度数据过期清理是否正常工作
|
||
|
||
### 3. 性能观察
|
||
- [ ] 观察首页渲染速度
|
||
- [ ] 检查浏览器内存占用
|
||
- [ ] 确认组件图标正常显示
|
||
|
||
## 📝 技术说明
|
||
|
||
### markRaw 的作用
|
||
`markRaw()` 是 Vue 3 提供的工具函数,用于标记一个对象,使其永远不会被转换为响应式对象。
|
||
|
||
**适用场景:**
|
||
1. 组件定义对象(如本次修复)
|
||
2. 大型不可变数据结构
|
||
3. 第三方库实例
|
||
4. DOM 元素引用
|
||
|
||
**优势:**
|
||
- 减少响应式系统的追踪开销
|
||
- 避免不必要的深度响应式转换
|
||
- 提升性能,特别是在处理大量数据时
|
||
|
||
### JavaScript ASI(自动分号插入)陷阱
|
||
这是一个经典的 JavaScript 陷阱,值得所有开发者注意:
|
||
|
||
**问题场景:**
|
||
当一行以 `(`, `[`, `` ` ``, `+`, `-`, `/` 等符号开头时,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())
|
||
```
|
||
|
||
**在本项目中的体现:**
|
||
```javascript
|
||
// ❌ 会被解析为: ref(...)((() => {})())
|
||
const savedProgress = ref(...)
|
||
(() => {})()
|
||
|
||
// ✅ 正确:添加分号
|
||
const savedProgress = ref(...);
|
||
(() => {})()
|
||
```
|
||
|
||
**最佳实践:**
|
||
1. **使用一致的分号风格**:要么总是加,要么总是不加(配合 linter)
|
||
2. **IIFE 前加分号**:如果不用分号风格,在 IIFE 前加 `;` 保护
|
||
3. **使用 ESLint**:配置 `semi` 规则强制统一风格
|
||
4. **代码审查**:特别注意以 `(` 或 `[` 开头的行
|
||
|
||
### 变量定义顺序的重要性
|
||
在 Pinia setup store 中,虽然闭包允许函数引用后面定义的变量,但:
|
||
1. `computed` 属性可能在定义时就触发计算
|
||
2. IIFE(立即执行函数)会在定义时立即执行
|
||
3. 某些情况下可能导致访问未初始化的变量
|
||
|
||
**最佳实践:**
|
||
- 将被多处引用的状态变量定义在前面
|
||
- 确保在使用前完成初始化
|
||
- 特别注意 `computed` 和 IIFE 的执行时机
|
||
- 在 `ref()` 等返回对象的调用后,如果紧跟 IIFE,必须加分号
|
||
|
||
## 🎯 版本信息
|
||
- **版本号:** v3.13.5.1
|
||
- **修复类型:** 紧急修复
|
||
- **优先级:** 🔴 高(阻塞性错误)
|
||
- **向后兼容:** ✅ 完全兼容
|
||
|
||
## 📌 相关文档
|
||
- [v3.13.5 - 内存清理机制优化](./内存清理机制优化v3.13.5.md)
|
||
- [Vue 3 - markRaw API 文档](https://vuejs.org/api/reactivity-advanced.html#markraw)
|
||
- [Pinia - Setup Stores](https://pinia.vuejs.org/core-concepts/#setup-stores)
|
||
|