Files
xyzw_web_helper/MD说明文件夹/紧急修复-变量作用域和性能警告v3.13.5.1.md
2025-10-17 20:56:50 +08:00

8.6 KiB
Raw Blame History

紧急修复 - 变量作用域和性能警告 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

技术细节:

  • ASIAutomatic 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 中的 featureCardsfeatures 数组包含了组件对象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 行):

// 任务执行历史记录最近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

修改内容:

  1. 导入 markRaw 函数
  2. 使用 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, 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 可能不会在上一行末尾自动插入分号。

危险示例:

// ❌ 错误:会被解析为 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(...);
(() => {})()

最佳实践:

  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
  • 修复类型: 紧急修复
  • 优先级: 🔴 高(阻塞性错误)
  • 向后兼容: 完全兼容

📌 相关文档