Angular倒计时实现引发浏览器无限加载,求interval替代方案
Angular倒计时异常排查及替代实现方案
问题描述
我在Angular中实现的倒计时功能存在异常:当浏览器完全加载完成后,先进入个人资料页再返回首页时,倒计时可正常显示;但直接刷新首页时,浏览器会持续无限加载。当前实现代码如下:
/* eslint-disable @typescript-eslint/no-unused-vars */ import { Component, OnInit, OnDestroy, ViewChild, afterNextRender, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { Carousel, CarouselModule } from 'primeng/carousel'; import { ButtonModule } from 'primeng/button'; import { TagModule } from 'primeng/tag'; import { AuctionService } from '../../../services/auction/auction.service'; import { interval, Subscription } from 'rxjs'; interface Auction { id: number; auction_end_date: string; timeRemaining?: string; [key: string]: any; } @Component({ selector: 'app-auctions', standalone: true, imports: [ CommonModule, RouterModule, CarouselModule, ButtonModule, TagModule, ], templateUrl: './auctions.component.html', styleUrl: './auctions.component.scss', }) export class AuctionsComponent implements OnInit { @ViewChild('carousel') carousel!: Carousel; @ViewChild('carouselFuture') carouselFuture!: Carousel; liveAuctions: Auction[] = []; futureAuctions: Auction[] = []; groupedFutureAuctions: any[][] = []; responsiveOptions: any[] | undefined; private countdownSubscription: Subscription | null = null; constructor(private auctionService: AuctionService) {} ngOnInit(): void { this.responsiveOptions = [ { breakpoint: '1199px', numVisible: 1, numScroll: 1, }, { breakpoint: '991px', numVisible: 2, numScroll: 1, }, { breakpoint: '767px', numVisible: 1, numScroll: 1, }, ]; this.fetchLiveAuctions(); this.fetchFutureAuctions(); } startCountdown(): void { this.countdownSubscription = interval(1000).subscribe(() => { this.liveAuctions = this.liveAuctions.map((auction) => ({ ...auction, timeRemaining: this.calculateTimeRemaining(auction.auction_end_date), })); this.futureAuctions = this.futureAuctions.map((auction: Auction) => ({ ...auction, timeRemaining: this.calculateTimeRemaining(auction.auction_end_date), })); this.groupFutureAuctions(); }); } groupFutureAuctions(): void { this.groupedFutureAuctions = []; for (let i = 0; i < this.futureAuctions.length; i += 4) { this.groupedFutureAuctions.push(this.futureAuctions.slice(i, i + 4)); } } fetchLiveAuctions(): void { this.auctionService .getLiveAuctions() .then((result: any) => { // console.log('Result in fetch live auctions', result.data); if (result?.data) { this.liveAuctions = result.data.map((auction: Auction) => ({ ...auction, timeRemaining: this.calculateTimeRemaining( auction.auction_end_date ), })); } this.startCountdown(); }) .catch((error) => { console.error('Error fetching live auctions:', error); }); } fetchFutureAuctions(): void { this.auctionService .getFutureAuctions() .then((result: any) => { console.log('Result in fetch future auctions', result.data); if (result?.data) { this.futureAuctions = result.data.map((auction: Auction) => ({ ...auction, timeRemaining: this.calculateTimeRemaining( auction.auction_end_date ), })); } this.groupFutureAuctions(); }) .catch((error) => { console.error('Error fetching live auctions:', error); }); } calculateTimeRemaining(endDate: string): string { if (!endDate) return 'Invalid date'; const end = new Date(endDate).getTime(); if (isNaN(end)) return 'Invalid date'; const now = Date.now(); const diff = end - now; if (diff <= 0) return 'Auction ended'; const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); return `${days}D ${hours}H ${minutes}M ${seconds}S`; } prevLive(event: Event): void { event.preventDefault(); if (event instanceof MouseEvent || event instanceof TouchEvent) { this.carousel?.navBackward(event); } } nextLive(event: Event): void { event.preventDefault(); if (event instanceof MouseEvent || event instanceof TouchEvent) { this.carousel?.navForward(event); } } prevFuture(event: Event): void { event.preventDefault(); if (event instanceof MouseEvent || event instanceof TouchEvent) { this.carouselFuture?.navBackward(event); } } nextFuture(event: Event): void { event.preventDefault(); if (event instanceof MouseEvent || event instanceof TouchEvent) { this.carouselFuture?.navForward(event); } } getSeverity(status: string) { switch (status) { case 'INSTOCK': return 'success'; case 'LOWSTOCK': return 'warning'; case 'OUTOFSTOCK': return 'danger'; default: return 'info'; } } trackByAuctionId(index: number, auction: any): number { return auction.id; } }
请问除了使用interval外,还有其他实现该倒计时功能的方法吗?
先解决当前无限加载问题
先排查你代码里导致异常的核心原因:
- 未清理订阅:组件导入了
OnDestroy但没实现,countdownSubscription在组件销毁时没取消,刷新时可能引发内存泄漏和循环更新。 - 重复启动订阅:每次
fetchLiveAuctions成功都会启动新的interval,刷新时可能叠加多个订阅,导致频繁触发变更检测。 - 不必要的数组重生成:每秒都用
map重新创建数组,加重视图更新负担。
先修复这些问题:
// 实现OnDestroy接口,清理订阅 export class AuctionsComponent implements OnInit, OnDestroy { // ... 现有代码 ngOnDestroy(): void { this.countdownSubscription?.unsubscribe(); } startCountdown(): void { // 先取消已有订阅,避免叠加 this.countdownSubscription?.unsubscribe(); this.countdownSubscription = interval(1000).subscribe(() => { // 优化:直接修改数组元素,避免重生成数组 this.liveAuctions.forEach(auction => { auction.timeRemaining = this.calculateTimeRemaining(auction.auction_end_date); }); this.futureAuctions.forEach(auction => { auction.timeRemaining = this.calculateTimeRemaining(auction.auction_end_date); }); this.groupFutureAuctions(); }); } }
替代interval的倒计时实现方法
1. 递归setTimeout实现
相比interval,递归setTimeout能避免时间累积偏差,且更灵活:
private countdownTimer: number | null = null; startCountdown(): void { // 清理已有定时器 if (this.countdownTimer) clearTimeout(this.countdownTimer); const updateCountdown = () => { // 更新倒计时逻辑 this.liveAuctions.forEach(auction => { auction.timeRemaining = this.calculateTimeRemaining(auction.auction_end_date); }); this.futureAuctions.forEach(auction => { auction.timeRemaining = this.calculateTimeRemaining(auction.auction_end_date); }); this.groupFutureAuctions(); // 递归调用,确保每次间隔1秒 this.countdownTimer = setTimeout(updateCountdown, 1000); }; // 立即执行一次,避免初始延迟 updateCountdown(); } ngOnDestroy(): void { if (this.countdownTimer) clearTimeout(this.countdownTimer); }
2. Angular async管道结合自定义Observable
利用async管道自动管理订阅,减少手动操作:
// 组件内创建倒计时Observable countdown$: Observable<number> = interval(1000);
模板中直接使用,不需要手动管理订阅:
<div *ngFor="let auction of liveAuctions; trackBy: trackByAuctionId"> {{ calculateTimeRemaining(auction.auction_end_date) }} </div>
async管道会在组件销毁时自动取消订阅,省掉手动清理的步骤。
3. requestAnimationFrame高精度实现
适合对时间精度要求高的场景,避免interval的时间漂移:
private rafId: number | null = null; startCountdown(): void { if (this.rafId) cancelAnimationFrame(this.rafId); let lastUpdateTime = Date.now(); const updateCountdown = () => { const now = Date.now(); // 确保至少间隔1秒才更新 if (now - lastUpdateTime >= 1000) { this.liveAuctions.forEach(auction => { auction.timeRemaining = this.calculateTimeRemaining(auction.auction_end_date); }); this.futureAuctions.forEach(auction => { auction.timeRemaining = this.calculateTimeRemaining(auction.auction_end_date); }); this.groupFutureAuctions(); lastUpdateTime = now; } this.rafId = requestAnimationFrame(updateCountdown); }; updateCountdown(); } ngOnDestroy(): void { if (this.rafId) cancelAnimationFrame(this.rafId); }
4. RxJS timer操作符
和interval类似,但支持设置初始延迟,适合需要立即更新的场景:
startCountdown(): void { this.countdownSubscription?.unsubscribe(); // 初始延迟0,之后每秒执行一次 this.countdownSubscription = timer(0, 1000).subscribe(() => { this.liveAuctions.forEach(auction => { auction.timeRemaining = this.calculateTimeRemaining(auction.auction_end_date); }); this.futureAuctions.forEach(auction => { auction.timeRemaining = this.calculateTimeRemaining(auction.auction_end_date); }); this.groupFutureAuctions(); }); }
内容的提问来源于stack exchange,提问作者Sachin Kizhakkepurath
相关产品推荐
相关产品推荐

