检查Panel的关闭按钮执行关闭的整个过程,解决可能的内存泄漏

This commit is contained in:
zqm
2025-11-20 10:21:05 +08:00
parent 5e60553a04
commit f19f8fadff
2 changed files with 382 additions and 38 deletions

View File

@@ -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,6 +445,7 @@ const onDragEnd = () => {
* 监听面板关闭事件,更新组件状态(可选)
*/
const setupEventListeners = () => {
try {
// 监听面板最大化同步事件
const unsubscribeMaximizeSync = onEvent(EVENT_TYPES.PANEL_MAXIMIZE_SYNC, (data) => {
if (data.panelId === props.id) {
@@ -317,25 +454,110 @@ const setupEventListeners = () => {
}
})
subscriptions.set('maximizeSync', unsubscribeMaximizeSync)
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}] 开始清理所有事件监听器...`)
}
// 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)
}
}
})
</script>

View File

@@ -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) {