You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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外,还有其他实现该倒计时功能的方法吗?


先解决当前无限加载问题

先排查你代码里导致异常的核心原因:

  1. 未清理订阅:组件导入了OnDestroy但没实现,countdownSubscription在组件销毁时没取消,刷新时可能引发内存泄漏和循环更新。
  2. 重复启动订阅:每次fetchLiveAuctions成功都会启动新的interval,刷新时可能叠加多个订阅,导致频繁触发变更检测。
  3. 不必要的数组重生成:每秒都用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 21:08:11