如何实现用户无操作指定时长后循环弹出模态框,现有代码仅首次生效问题排查
问题根因排查
- 订阅逻辑失效:组件仅在
ngOnInit阶段订阅了第一次生成的observeable$,每次触发弹窗后调用initScreenListen会重新赋值服务里的observeable$,新的 observable 没有被任何订阅者监听,自然不会触发后续逻辑。 - 事件监听异常:
merge里重复注册了2次document的MOUSE_MOVE事件,还有拼写错误BanMouseEvent.CLIK应为BanMouseEvent.CLICK,多次调用initScreenListen会持续注册新的事件监听,导致内存泄漏、逻辑冲突。 - 无旧订阅清理逻辑:每次重新初始化监听时,没有取消之前的事件订阅、定时器订阅,旧的逻辑还在后台运行,导致计数混乱。
修复方案
第一步:修改服务代码
import { Subscription, Subject, Observable } from 'rxjs'; export class InactivityListenService { private destroySub = new Subscription(); // 用Subject对外统一通知超时事件 public inactivityTimeout$ = new Subject<void>(); // 无操作超时时间,单位:秒 private inactivityTime = 5; public initScreenListen(): void { // 先清理之前的所有订阅,避免旧逻辑干扰 this.destroySub.unsubscribe(); this.destroySub = new Subscription(); const mergedObservable$ = merge( fromEvent(document, BanKeyboardEvent.KEY_DOWN), fromEvent(document, BanMouseEvent.MOUSE_DOWN), fromEvent(document, BanMouseEvent.MOUSE_MOVE), fromEvent(document, BanMouseEvent.WHEEL), fromEvent(document, BanMouseEvent.CLICK), // 修复拼写错误 fromEvent(window, BanMouseEvent.MOUSE_MOVE) ); this.startTimer(mergedObservable$); } private startTimer(mergedObservable$: Observable<Event>): void { this.ngZone.runOutsideAngular(() => { const timerSub = mergedObservable$.pipe( // 每次检测到用户操作就重置定时器 switchMap(() => interval(1000).pipe(take(this.inactivityTime))), tap((value) => this.isItTimeToShowPopUp(value)), skipWhile(x => x !== this.inactivityTime - 1) ).subscribe(() => { // 超时后对外发送通知 this.inactivityTimeout$.next(); }); this.destroySub.add(timerSub); }); } private isItTimeToShowPopUp(val: number): void { this.timeLeftForInactive = this.inactivityTime - val; if (this.timeLeftForInactive <= 13) { this.timeLapsedSinceInactivity = this.timeLeftForInactive; this.ref.tick(); console.log(this.timeLeftForInactive); } } // 组件销毁时调用,全量清理资源 public destroy(): void { this.destroySub.unsubscribe(); this.inactivityTimeout$.complete(); } }
第二步:修改组件代码
import { Subscription } from 'rxjs'; // 组件内声明销毁订阅容器 private componentDestroySub = new Subscription(); public ngOnInit(): void { this.inactivityListenService.initScreenListen(); // 只需要订阅一次超时通知即可 const timeoutSub = this.inactivityListenService.inactivityTimeout$.subscribe(() => { console.log('here show modal.....'); // 弹窗后重新初始化监听即可,不需要重复订阅 this.inactivityListenService.initScreenListen(); }); this.componentDestroySub.add(timeoutSub); } // 组件销毁钩子清理所有订阅,避免内存泄漏 public ngOnDestroy(): void { this.componentDestroySub.unsubscribe(); this.inactivityListenService.destroy(); }
优化说明
- 不需要对外暴露内部的可观察对象,用统一的通知Subject对外传递事件,避免订阅管理混乱
- 所有订阅都集中在销毁容器里统一管理,销毁时一次性清理,不会出现残留的后台逻辑
- 修复了事件拼写错误和重复注册问题,避免多余的事件触发
内容的提问来源于stack exchange,提问作者Julio Rodríguez
相关产品推荐
相关产品推荐

