fix(notification): 修复内存泄漏和完善定时器管理
- 添加音频资源清理(组件卸载时释放 audioRef) - 添加通知自动关闭定时器跟踪(Map 数据结构) - removeNotification 自动清理对应定时器 - clearAllNotifications 批量清理所有定时器 - 增强事件去重机制(处理缺失 ID 的边界情况) - 添加浏览器通知权限状态同步(监听 focus 事件) - 移除废弃的通知分发策略注释代码 修复 React 严格模式下的内存泄漏问题,确保所有资源正确清理。 🔧 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,7 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态
|
const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态
|
||||||
const processedEventIds = useRef(new Set()); // 用于Socket层去重,记录已处理的事件ID
|
const processedEventIds = useRef(new Set()); // 用于Socket层去重,记录已处理的事件ID
|
||||||
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏
|
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏
|
||||||
|
const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器
|
||||||
|
|
||||||
// ⚡ 使用权限引导管理 Hook
|
// ⚡ 使用权限引导管理 Hook
|
||||||
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
|
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
|
||||||
@@ -71,9 +72,20 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
audioRef.current = new Audio(notificationSound);
|
audioRef.current = new Audio(notificationSound);
|
||||||
audioRef.current.volume = 0.5;
|
audioRef.current.volume = 0.5;
|
||||||
|
logger.info('NotificationContext', 'Audio initialized');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('NotificationContext', 'Audio initialization failed', error);
|
logger.error('NotificationContext', 'Audio initialization failed', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理函数:释放音频资源
|
||||||
|
return () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
audioRef.current.src = '';
|
||||||
|
audioRef.current = null;
|
||||||
|
logger.info('NotificationContext', 'Audio resources cleaned up');
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +116,13 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
const removeNotification = useCallback((id, wasClicked = false) => {
|
const removeNotification = useCallback((id, wasClicked = false) => {
|
||||||
logger.info('NotificationContext', 'Removing notification', { id, wasClicked });
|
logger.info('NotificationContext', 'Removing notification', { id, wasClicked });
|
||||||
|
|
||||||
|
// 清理对应的定时器
|
||||||
|
if (notificationTimers.current.has(id)) {
|
||||||
|
clearTimeout(notificationTimers.current.get(id));
|
||||||
|
notificationTimers.current.delete(id);
|
||||||
|
logger.info('NotificationContext', 'Cleared auto-close timer', { id });
|
||||||
|
}
|
||||||
|
|
||||||
// 监控埋点:追踪关闭(非点击的情况)
|
// 监控埋点:追踪关闭(非点击的情况)
|
||||||
setNotifications(prev => {
|
setNotifications(prev => {
|
||||||
const notification = prev.find(n => n.id === id);
|
const notification = prev.find(n => n.id === id);
|
||||||
@@ -119,6 +138,14 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
*/
|
*/
|
||||||
const clearAllNotifications = useCallback(() => {
|
const clearAllNotifications = useCallback(() => {
|
||||||
logger.info('NotificationContext', 'Clearing all notifications');
|
logger.info('NotificationContext', 'Clearing all notifications');
|
||||||
|
|
||||||
|
// 清理所有定时器
|
||||||
|
notificationTimers.current.forEach((timerId, id) => {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
logger.info('NotificationContext', 'Cleared timer during clear all', { id });
|
||||||
|
});
|
||||||
|
notificationTimers.current.clear();
|
||||||
|
|
||||||
setNotifications([]);
|
setNotifications([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -446,9 +473,16 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
|
|
||||||
// 自动关闭
|
// 自动关闭
|
||||||
if (newNotification.autoClose && newNotification.autoClose > 0) {
|
if (newNotification.autoClose && newNotification.autoClose > 0) {
|
||||||
setTimeout(() => {
|
const timerId = setTimeout(() => {
|
||||||
removeNotification(newNotification.id);
|
removeNotification(newNotification.id);
|
||||||
}, newNotification.autoClose);
|
}, newNotification.autoClose);
|
||||||
|
|
||||||
|
// 将定时器ID保存到Map中
|
||||||
|
notificationTimers.current.set(newNotification.id, timerId);
|
||||||
|
logger.info('NotificationContext', 'Set auto-close timer', {
|
||||||
|
id: newNotification.id,
|
||||||
|
delay: newNotification.autoClose
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [playNotificationSound, removeNotification]);
|
}, [playNotificationSound, removeNotification]);
|
||||||
|
|
||||||
@@ -548,34 +582,11 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
|
|
||||||
const isPageHidden = document.hidden; // 页面是否在后台
|
const isPageHidden = document.hidden; // 页面是否在后台
|
||||||
|
|
||||||
// ========== 原分发策略(按优先级区分)- 已废弃 ==========
|
// ========== 通知分发策略(区分前后台) ==========
|
||||||
// 策略 1: 紧急通知 - 双重保障(浏览器 + 网页)
|
// 策略: 根据页面可见性智能分发通知
|
||||||
// if (priority === PRIORITY_LEVELS.URGENT) {
|
// - 页面在后台: 发送浏览器通知(系统级提醒)
|
||||||
// logger.info('NotificationContext', 'Urgent notification: sending browser + web');
|
// - 页面在前台: 发送网页通知(页面内 Toast)
|
||||||
// // 总是发送浏览器通知
|
// 注: 不再区分优先级,统一使用前后台策略
|
||||||
// sendBrowserNotification(newNotification);
|
|
||||||
// // 如果在前台,也显示网页通知
|
|
||||||
// if (!isPageHidden) {
|
|
||||||
// addWebNotification(newNotification);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// 策略 2: 重要通知 - 智能切换(后台=浏览器,前台=网页)
|
|
||||||
// else if (priority === PRIORITY_LEVELS.IMPORTANT) {
|
|
||||||
// if (isPageHidden) {
|
|
||||||
// logger.info('NotificationContext', 'Important notification (background): sending browser');
|
|
||||||
// sendBrowserNotification(newNotification);
|
|
||||||
// } else {
|
|
||||||
// logger.info('NotificationContext', 'Important notification (foreground): sending web');
|
|
||||||
// addWebNotification(newNotification);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// 策略 3: 普通通知 - 仅网页通知
|
|
||||||
// else {
|
|
||||||
// logger.info('NotificationContext', 'Normal notification: sending web only');
|
|
||||||
// addWebNotification(newNotification);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ========== 新分发策略(仅区分前后台) ==========
|
|
||||||
if (isPageHidden) {
|
if (isPageHidden) {
|
||||||
// 页面在后台:发送浏览器通知
|
// 页面在后台:发送浏览器通知
|
||||||
logger.info('NotificationContext', 'Page hidden: sending browser notification');
|
logger.info('NotificationContext', 'Page hidden: sending browser notification');
|
||||||
@@ -688,7 +699,18 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
logger.info('NotificationContext', 'Received new event', data);
|
logger.info('NotificationContext', 'Received new event', data);
|
||||||
|
|
||||||
// ========== Socket层去重检查 ==========
|
// ========== Socket层去重检查 ==========
|
||||||
const eventId = data.id || `${data.type}_${data.publishTime}`;
|
// 生成更健壮的事件ID
|
||||||
|
const eventId = data.id ||
|
||||||
|
`${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
// 如果缺少原始ID,记录警告
|
||||||
|
if (!data.id) {
|
||||||
|
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
|
||||||
|
eventId,
|
||||||
|
eventType: data.type,
|
||||||
|
title: data.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (processedEventIds.current.has(eventId)) {
|
if (processedEventIds.current.has(eventId)) {
|
||||||
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
|
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
|
||||||
@@ -744,6 +766,19 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
return () => {
|
return () => {
|
||||||
logger.info('NotificationContext', 'Cleaning up socket connection');
|
logger.info('NotificationContext', 'Cleaning up socket connection');
|
||||||
|
|
||||||
|
// 清理 reconnected 状态定时器
|
||||||
|
if (reconnectedTimerRef.current) {
|
||||||
|
clearTimeout(reconnectedTimerRef.current);
|
||||||
|
reconnectedTimerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理所有通知的自动关闭定时器
|
||||||
|
notificationTimers.current.forEach((timerId, id) => {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
logger.info('NotificationContext', 'Cleared timer during cleanup', { id });
|
||||||
|
});
|
||||||
|
notificationTimers.current.clear();
|
||||||
|
|
||||||
socket.off('connect');
|
socket.off('connect');
|
||||||
socket.off('disconnect');
|
socket.off('disconnect');
|
||||||
socket.off('connect_error');
|
socket.off('connect_error');
|
||||||
@@ -837,6 +872,48 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步浏览器通知权限状态
|
||||||
|
* 场景:
|
||||||
|
* 1. 用户在其他标签页授权后返回
|
||||||
|
* 2. 用户在浏览器设置中修改权限
|
||||||
|
* 3. 页面长时间打开后权限状态变化
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const checkPermission = () => {
|
||||||
|
const current = browserNotificationService.getPermissionStatus();
|
||||||
|
if (current !== browserPermission) {
|
||||||
|
logger.info('NotificationContext', 'Browser permission changed', {
|
||||||
|
old: browserPermission,
|
||||||
|
new: current
|
||||||
|
});
|
||||||
|
setBrowserPermission(current);
|
||||||
|
|
||||||
|
// 如果权限被授予,显示成功提示
|
||||||
|
if (current === 'granted' && browserPermission !== 'granted') {
|
||||||
|
toast({
|
||||||
|
title: '桌面通知已开启',
|
||||||
|
description: '您现在可以在后台接收重要通知',
|
||||||
|
status: 'success',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面聚焦时检查
|
||||||
|
window.addEventListener('focus', checkPermission);
|
||||||
|
|
||||||
|
// 定期检查(可选,用于捕获浏览器设置中的变化)
|
||||||
|
const intervalId = setInterval(checkPermission, 30000); // 每30秒检查一次
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', checkPermission);
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [browserPermission, toast]);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
notifications,
|
notifications,
|
||||||
isConnected,
|
isConnected,
|
||||||
|
|||||||
Reference in New Issue
Block a user