diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue b/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue index 40fffdd..6be5fa6 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue @@ -166,8 +166,9 @@ const props = defineProps({ // 改为通过DOM动态获取当前所在区域 }); -// 事件订阅管理 -const subscriptions = new Map(); +// 事件订阅管理 - 使用Set避免key冲突,并添加唯一标识符 +const subscriptions = new Set(); +const subscriptionRegistry = new Map(); // 用于追踪订阅详细信息 // 动态获取当前面板所在的Area ID const getCurrentAreaId = () => { @@ -234,16 +235,155 @@ const onToggleToolbar = () => { }; // 拖拽相关状态 -let isDragging = false; +let isDragging = false + +// 全局内存泄漏保护机制 +if (!window.__panelMemoryProtection) { + window.__panelMemoryProtection = { + // 存储所有面板组件实例追踪信息 + panelInstances: new Map(), + + // 定时检测内存泄漏(开发环境) + startLeakDetection() { + if (import.meta.env.DEV) { + setInterval(() => { + this.detectMemoryLeaks() + }, 30000) // 每30秒检测一次 + } + }, + + // 检测内存泄漏 + detectMemoryLeaks() { + const activePanels = window.__panelDragHandlers ? window.__panelDragHandlers.size : 0 + const registeredPanels = this.panelInstances.size + + if (activePanels !== registeredPanels) { + console.warn(`[内存泄漏检测] 发现面板内存不一致 - 活动拖拽: ${activePanels}, 注册实例: ${registeredPanels}`) + + // 清理 orphaned handlers + if (window.__panelDragHandlers && activePanels > 0) { + window.__panelDragHandlers.forEach((handlers, panelId) => { + if (!this.panelInstances.has(panelId)) { + console.warn(`[内存泄漏检测] 清理orphaned handler: ${panelId}`) + document.removeEventListener('mousemove', handlers.dragMoveHandler, false) + document.removeEventListener('mouseup', handlers.dragEndHandler, false) + document.removeEventListener('mouseleave', handlers.dragEndHandler, false) + window.__panelDragHandlers.delete(panelId) + } + }) + } + } + }, + + // 注册面板实例 + registerPanel(panelId) { + this.panelInstances.set(panelId, { + createdAt: Date.now(), + lastActivity: Date.now() + }) + }, + + // 注销面板实例 + unregisterPanel(panelId) { + this.panelInstances.delete(panelId) + }, + + // 更新活动状态 + updateActivity(panelId) { + const panel = this.panelInstances.get(panelId) + if (panel) { + panel.lastActivity = Date.now() + } + } + } + + // 启动内存泄漏检测 + window.__panelMemoryProtection.startLeakDetection() +} + +/** + * 添加Document拖拽事件监听器 + */ +const addDocumentDragListeners = () => { + // 移除可能存在的旧监听器 + cleanupDragEventListeners() + + // 使用组件实例标识符确保清理正确性 + const componentId = `panel_${props.id}` + const dragMoveHandler = (e) => onDragMove(e) + const dragEndHandler = (e) => onDragEnd(e) + + // 将处理函数绑定到组件作用域,避免匿名函数导致的清理问题 + if (!window.__panelDragHandlers) { + window.__panelDragHandlers = new Map() + } + + window.__panelDragHandlers.set(componentId, { + dragMoveHandler, + dragEndHandler + }) + + document.addEventListener('mousemove', dragMoveHandler, false) + document.addEventListener('mouseup', dragEndHandler, false) + document.addEventListener('mouseleave', dragEndHandler, false) + + if (import.meta.env.DEV) { + console.log(`[Panel:${props.id}] Document拖拽事件监听器已添加: ${componentId}`) + } +} + +/** + * 清理Document拖拽事件监听器 + */ +const cleanupDragEventListeners = () => { + try { + const componentId = `panel_${props.id}` + + // 从全局拖拽处理函数映射中获取处理函数 + const handlers = window.__panelDragHandlers?.get(componentId) + + if (handlers) { + // 使用正确的处理函数引用进行清理 + document.removeEventListener('mousemove', handlers.dragMoveHandler, false) + document.removeEventListener('mouseup', handlers.dragEndHandler, false) + document.removeEventListener('mouseleave', handlers.dragEndHandler, false) + + // 从映射中移除 + window.__panelDragHandlers.delete(componentId) + + // 清理映射,如果为空则删除整个映射 + if (window.__panelDragHandlers.size === 0) { + delete window.__panelDragHandlers + } + + if (import.meta.env.DEV) { + console.log(`[Panel:${props.id}] Document拖拽事件监听器已清理: ${componentId}`) + } + } + + // 立即重置拖拽状态,确保清理完整性 + isDragging = false + + } catch (error) { + console.warn(`[Panel:${props.id}] 清理拖拽事件监听器时出错:`, error) + + // 发生错误时仍然重置状态 + isDragging = false + } +} // 拖拽开始 const onDragStart = (e) => { // 只有当点击的是标题栏区域(不是按钮)时才触发拖拽 if (!e.target.closest('.title-bar-buttons') && !e.target.closest('button')) { - isDragging = true; + // 1. 立即重置之前的拖拽状态 + isDragging = false + cleanupDragEventListeners() + + isDragging = true console.log(`[Panel:${props.id}] 开始拖拽`) - // 使用事件总线触发拖拽开始事件 + // 2. 使用事件总线触发拖拽开始事件 emitEvent(EVENT_TYPES.PANEL_DRAG_START, { panelId: props.id, areaId: getCurrentAreaId(), @@ -253,14 +393,12 @@ const onDragStart = (e) => { source: { component: 'Panel', panelId: props.id } }) - // 防止文本选择和默认行为 - e.preventDefault(); - e.stopPropagation(); + // 3. 防止文本选择和默认行为 + e.preventDefault() + e.stopPropagation() - // 将鼠标移动和释放事件绑定到document,确保拖拽的连续性 - document.addEventListener('mousemove', onDragMove); - document.addEventListener('mouseup', onDragEnd); - document.addEventListener('mouseleave', onDragEnd); + // 4. 添加Document事件监听器,使用一次性变量避免内存泄漏 + addDocumentDragListeners() } }; @@ -298,10 +436,8 @@ const onDragEnd = () => { source: { component: 'Panel', panelId: props.id } }) - // 拖拽结束后移除事件监听器 - document.removeEventListener('mousemove', onDragMove); - document.removeEventListener('mouseup', onDragEnd); - document.removeEventListener('mouseleave', onDragEnd); + // 使用统一的清理方法,确保一致性和完整性 + cleanupDragEventListeners() } }; @@ -309,33 +445,119 @@ const onDragEnd = () => { * 监听面板关闭事件,更新组件状态(可选) */ const setupEventListeners = () => { - // 监听面板最大化同步事件 - const unsubscribeMaximizeSync = onEvent(EVENT_TYPES.PANEL_MAXIMIZE_SYNC, (data) => { - if (data.panelId === props.id) { - // 这里可以添加最大化状态同步的逻辑 - console.log(`[Panel:${props.id}] 收到最大化同步事件`) - } - }) - - subscriptions.set('maximizeSync', unsubscribeMaximizeSync) + try { + // 监听面板最大化同步事件 + const unsubscribeMaximizeSync = onEvent(EVENT_TYPES.PANEL_MAXIMIZE_SYNC, (data) => { + if (data.panelId === props.id) { + // 这里可以添加最大化状态同步的逻辑 + console.log(`[Panel:${props.id}] 收到最大化同步事件`) + } + }) + + const subscriptionId = `maximizeSync_${props.id}_${Date.now()}` + subscriptions.add(unsubscribeMaximizeSync) + subscriptionRegistry.set(subscriptionId, { + unsubscribe: unsubscribeMaximizeSync, + name: 'maximizeSync', + createdAt: Date.now() + }) + + console.log(`[Panel:${props.id}] 事件监听器注册完成,ID: ${subscriptionId}`) + + } catch (error) { + console.error(`[Panel:${props.id}] 注册事件监听器失败:`, error) + } } /** - * 清理所有事件订阅 + * 增强版清理事件监听器,返回清理结果统计 */ const cleanupEventListeners = () => { - subscriptions.forEach((unsubscribe) => { - if (typeof unsubscribe === 'function') { - unsubscribe() + const cleanupResult = { + eventSubscriptions: 0, + documentListeners: 0, + errors: [] + } + + try { + if (import.meta.env.DEV) { + console.log(`[Panel:${props.id}] 开始清理所有事件监听器...`) } - }) - subscriptions.clear() + + // 1. 清理事件订阅 + let unsubscribeCount = 0 + const subscriptionsToCleanup = Array.from(subscriptions) + + subscriptionsToCleanup.forEach((subscription, index) => { + try { + if (subscription && typeof subscription === 'function') { + subscription() // 执行取消订阅函数 + unsubscribeCount++ + } else { + console.warn(`[Panel:${props.id}] 发现无效的订阅函数,索引: ${index}`) + } + } catch (error) { + console.warn(`[Panel:${props.id}] 取消订阅时出错,索引: ${index}:`, error) + cleanupResult.errors.push(`取消订阅错误 (${index}): ${error.message}`) + } + }) + + // 清空订阅集合和注册表 + subscriptions.clear() + subscriptionRegistry.clear() + + cleanupResult.eventSubscriptions = unsubscribeCount + if (import.meta.env.DEV) { + console.log(`[Panel:${props.id}] 已清理 ${unsubscribeCount} 个事件订阅`) + } + + } catch (error) { + console.error(`[Panel:${props.id}] 清理事件订阅时发生严重错误:`, error) + cleanupResult.errors.push(`事件订阅清理错误: ${error.message}`) + } + + try { + // 2. 清理Document事件监听器 + cleanupDragEventListeners() + cleanupResult.documentListeners = 3 // mousemove, mouseup, mouseleave + + } catch (error) { + console.error(`[Panel:${props.id}] 清理Document事件监听器时发生错误:`, error) + cleanupResult.errors.push(`Document事件清理错误: ${error.message}`) + } + + // 3. 重置状态 + try { + isDragging = false + if (import.meta.env.DEV) { + console.log(`[Panel:${props.id}] 拖拽状态已重置`) + } + } catch (error) { + cleanupResult.errors.push(`状态重置错误: ${error.message}`) + } + + // 4. 输出清理结果摘要 + if (import.meta.env.DEV) { + const totalErrors = cleanupResult.errors.length + if (totalErrors > 0) { + console.warn(`[Panel:${props.id}] 清理完成,存在 ${totalErrors} 个错误:`, cleanupResult.errors) + } else { + console.log(`[Panel:${props.id}] 清理完全成功,无错误`) + } + } + + return cleanupResult } // 生命周期钩子 onMounted(() => { console.log(`[Panel:${props.id}] 组件已挂载`) + // 注册到全局内存保护机制 + if (window.__panelMemoryProtection) { + window.__panelMemoryProtection.registerPanel(`panel_${props.id}`) + } + // 启用调试模式(开发环境) if (import.meta.env.DEV) { eventBus.setDebugMode(true) @@ -343,20 +565,61 @@ onMounted(() => { // 设置事件监听器 setupEventListeners() + + // 设置拖拽相关事件监听器 + addDocumentDragListeners() + + // 更新活动状态 + if (window.__panelMemoryProtection) { + window.__panelMemoryProtection.updateActivity(`panel_${props.id}`) + } + + if (import.meta.env.DEV) { + console.log(`[Panel:${props.id}] 所有监听器设置完成`) + } }) onUnmounted(() => { console.log(`[Panel:${props.id}] 组件即将卸载`) - // 清理事件监听器 - cleanupEventListeners() + // 立即注销全局内存保护机制 + if (window.__panelMemoryProtection) { + window.__panelMemoryProtection.unregisterPanel(`panel_${props.id}`) + } - // 确保拖拽状态已清理 - if (isDragging) { - document.removeEventListener('mousemove', onDragMove); - document.removeEventListener('mouseup', onDragEnd); - document.removeEventListener('mouseleave', onDragEnd); + try { + // 1. 立即设置标志位,防止新的异步操作 isDragging = false + + // 2. 同步清理所有可以直接清理的资源 + const cleanupResult = cleanupEventListeners() + + // 3. 记录清理结果 + if (import.meta.env.DEV) { + console.log(`[Panel:${props.id}] 组件清理结果:`, cleanupResult) + } + + // 4. 清理超时保护 - 简化版本,防止无限等待 + const cleanupTimeout = setTimeout(() => { + console.warn(`[Panel:${props.id}] 清理超时,但继续卸载`) + }, 200) // 缩短超时时间 + + // 5. 清理超时定时器,确保不会泄露 + setTimeout(() => { + clearTimeout(cleanupTimeout) + }, 250) + + } catch (error) { + console.error(`[Panel:${props.id}] 清理过程中出现异常:`, error) + + // 即使出现异常,也要尝试强制清理 + try { + isDragging = false + // 强制清理事件监听器 + cleanupEventListeners() + } catch (forceError) { + console.error(`[Panel:${props.id}] 强制清理也失败:`, forceError) + } } }) diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/eventBus.js b/AutoRobot/Windows/Robot/Web/src/DockLayout/eventBus.js index e32eabf..ab137be 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/eventBus.js +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/eventBus.js @@ -192,6 +192,87 @@ class EnhancedEventBus { // 创建全局事件总线实例 export const eventBus = new EnhancedEventBus() +// 扩展EnhancedEventBus类,添加自动清理和泄漏检测功能 +const originalOn = EnhancedEventBus.prototype.on +const originalClear = EnhancedEventBus.prototype.clear + +// 扩展构造方法 +EnhancedEventBus.prototype.constructor = function() { + this.cleanupInterval = null + this.startCleanupTimer() +} + +// 扩展on方法,添加泄漏检测 +EnhancedEventBus.prototype.on = function(eventType, callback, options = {}) { + const unsubscribe = originalOn.call(this, eventType, callback, options) + + // 监控监听器数量 + this.checkForListenerLeaks() + + return unsubscribe +} + +// 扩展clear方法,清理定时器 +EnhancedEventBus.prototype.clear = function() { + originalClear.call(this) + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval) + this.cleanupInterval = null + } +} + +// 添加定期清理定时器 +EnhancedEventBus.prototype.startCleanupTimer = function() { + this.cleanupInterval = setInterval(() => { + if (this.eventHistory && this.eventHistory.length > this.maxHistorySize * 0.8) { + this.eventHistory = this.eventHistory.slice(-this.maxHistorySize) + if (this.debugMode) { + console.log(`[EventBus:${this.instanceId}] 自动清理事件历史,当前记录数: ${this.eventHistory.length}`) + } + } + + // 检查监听器泄漏 + this.checkForListenerLeaks() + }, 30000) // 每30秒检查一次 +} + +// 检查监听器泄漏 +EnhancedEventBus.prototype.checkForListenerLeaks = function() { + const stats = this.getStats() + const totalListeners = Object.values(stats).reduce((sum, count) => sum + count, 0) + + // 检查是否有异常的监听器数量 + if (totalListeners > 100) { + console.warn(`[EventBus:${this.instanceId}] 检测到可能的监听器泄漏,总监听器数: ${totalListeners}`) + } + + return totalListeners +} + +// 获取增强的统计信息 +EnhancedEventBus.prototype.getStatsWithLeakDetection = function() { + const stats = this.getStats() + const totalListeners = this.checkForListenerLeaks() + + return { + ...stats, + totalListeners, + instanceId: this.instanceId, + eventHistorySize: this.eventHistory ? this.eventHistory.length : 0, + hasCleanupTimer: !!this.cleanupInterval + } +} + +// 销毁实例方法 +EnhancedEventBus.prototype.destroy = function() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval) + this.cleanupInterval = null + } + this.clear() + console.log(`[EventBus:${this.instanceId}] 实例已销毁`) +} + // 拖拽状态管理器 export class DragStateManager { constructor(eventBus) {