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 processedEventIds = useRef(new Set()); // 用于Socket层去重,记录已处理的事件ID
|
||||
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏
|
||||
const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器
|
||||
|
||||
// ⚡ 使用权限引导管理 Hook
|
||||
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
|
||||
@@ -71,9 +72,20 @@ export const NotificationProvider = ({ children }) => {
|
||||
try {
|
||||
audioRef.current = new Audio(notificationSound);
|
||||
audioRef.current.volume = 0.5;
|
||||
logger.info('NotificationContext', 'Audio initialized');
|
||||
} catch (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) => {
|
||||
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 => {
|
||||
const notification = prev.find(n => n.id === id);
|
||||
@@ -119,6 +138,14 @@ export const NotificationProvider = ({ children }) => {
|
||||
*/
|
||||
const clearAllNotifications = useCallback(() => {
|
||||
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([]);
|
||||
}, []);
|
||||
|
||||
@@ -446,9 +473,16 @@ export const NotificationProvider = ({ children }) => {
|
||||
|
||||
// 自动关闭
|
||||
if (newNotification.autoClose && newNotification.autoClose > 0) {
|
||||
setTimeout(() => {
|
||||
const timerId = setTimeout(() => {
|
||||
removeNotification(newNotification.id);
|
||||
}, 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]);
|
||||
|
||||
@@ -548,34 +582,11 @@ export const NotificationProvider = ({ children }) => {
|
||||
|
||||
const isPageHidden = document.hidden; // 页面是否在后台
|
||||
|
||||
// ========== 原分发策略(按优先级区分)- 已废弃 ==========
|
||||
// 策略 1: 紧急通知 - 双重保障(浏览器 + 网页)
|
||||
// if (priority === PRIORITY_LEVELS.URGENT) {
|
||||
// logger.info('NotificationContext', 'Urgent notification: sending browser + web');
|
||||
// // 总是发送浏览器通知
|
||||
// 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);
|
||||
// }
|
||||
|
||||
// ========== 新分发策略(仅区分前后台) ==========
|
||||
// ========== 通知分发策略(区分前后台) ==========
|
||||
// 策略: 根据页面可见性智能分发通知
|
||||
// - 页面在后台: 发送浏览器通知(系统级提醒)
|
||||
// - 页面在前台: 发送网页通知(页面内 Toast)
|
||||
// 注: 不再区分优先级,统一使用前后台策略
|
||||
if (isPageHidden) {
|
||||
// 页面在后台:发送浏览器通知
|
||||
logger.info('NotificationContext', 'Page hidden: sending browser notification');
|
||||
@@ -688,7 +699,18 @@ export const NotificationProvider = ({ children }) => {
|
||||
logger.info('NotificationContext', 'Received new event', data);
|
||||
|
||||
// ========== 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)) {
|
||||
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
|
||||
@@ -744,6 +766,19 @@ export const NotificationProvider = ({ children }) => {
|
||||
return () => {
|
||||
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('disconnect');
|
||||
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 = {
|
||||
notifications,
|
||||
isConnected,
|
||||
|
||||
Reference in New Issue
Block a user