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

Angular API调用每秒重复触发问题排查求助

API持续重复调用问题排查与修复

问题背景

将Angular静态数据表格改为通过API获取数据后,出现API每秒重复调用、数据不断刷新的问题,相关代码如下:

executions.service.ts(API调用逻辑)

import { Injectable, PipeTransform } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { debounceTime, delay, map, switchMap, tap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

interface SearchResult {
  jobs: any[];
  total: number;
}

interface State {
  page: number;
  pageSize: number;
  searchTerm: string;
  startIndex: number;
  endIndex: number;
  totalRecords: number;
}

const compare = (v1: string | number, v2: string | number) => v1 < v2 ? -1 : v1 > v2 ? 1 : 0;

function matches(job: any, term: string, pipe: PipeTransform) {
  return true;
}

@Injectable({
  providedIn: 'root'
})
export class AdvancedService {
  fetchedJobs: any[] = [];
  fetchedJobs$: Observable<any[]>;
  private _loading$ = new BehaviorSubject<boolean>(true);
  private _search$ = new Subject<void>();
  private _jobs$ = new BehaviorSubject<any[]>([]);
  private _total$ = new BehaviorSubject<number>(0);
  private _state: State = {
    page: 1,
    pageSize: 10,
    searchTerm: '',
    startIndex: 0,
    endIndex: 9,
    totalRecords: 0
  };

  constructor(private pipe: DecimalPipe,
    private http: HttpClient) {

    this.fetchedJobs$ = this.http.get<any[]>(`https://api.publicapis.org/entries`).pipe(
      tap((res: any[])=>{
        this.fetchedJobs=res;
    }));

    this._search$.pipe(
      tap(() => this._loading$.next(true)),
      debounceTime(200),
      switchMap(() => this._search()),
      delay(200),
      tap(() => this._loading$.next(false))
    ).subscribe(result => {
      this._jobs$.next(result.jobs);
      this._total$.next(result.total);
    });
    this._search$.next();
  }

  get jobs$() { return this._jobs$.asObservable(); }
  get total$() { return this._total$.asObservable(); }
  get loading$() { return this._loading$.asObservable(); }
  get page() { return this._state.page; }
  get pageSize() { return this._state.pageSize; }
  get searchTerm() { return this._state.searchTerm; }

  get startIndex() { return this._state.startIndex; }
  get endIndex() { return this._state.endIndex; }
  get totalRecords() { return this._state.totalRecords; }

  set page(page: number) { this._set({ page }); }
  set pageSize(pageSize: number) { this._set({ pageSize }); }
  set startIndex(startIndex: number) { this._set({ startIndex }); }
  set endIndex(endIndex: number) { this._set({ endIndex }); }
  set totalRecords(totalRecords: number) { this._set({ totalRecords }); }
  set searchTerm(searchTerm: string) { this._set({ searchTerm }); }

  private _set(patch: Partial<State>) {
    Object.assign(this._state, patch);
    this._search$.next();
  }

  private _search(): Observable<SearchResult> {
    const { pageSize, page, searchTerm } = this._state;

    return this.fetchedJobs$.pipe(
      map((res: any[]) => {

      let jobs = Object.values(res);

      jobs = jobs.filter(job => matches(job, searchTerm, this.pipe));
      const total = jobs.length;

      this.totalRecords = jobs.length;
      this._state.startIndex = (page - 1) * this.pageSize + 1;
      this._state.endIndex = (page - 1) * this.pageSize + this.pageSize;
      if (this.endIndex > this.totalRecords) {
        this.endIndex = this.totalRecords;
      }
      jobs = jobs.slice(this._state.startIndex - 1, this._state.endIndex);

      return  { jobs, total } as SearchResult;

    }));
  }
}

app.component.ts(订阅逻辑)

import { Component, OnInit, ViewChildren, QueryList } from '@angular/core';
import { Observable } from 'rxjs';
import { AdvancedService } from './executions.service';
import { CommonModule, DecimalPipe } from '@angular/common';

@Component({
  selector: 'app-component',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  imports: [CommonModule],
  providers: [AdvancedService, DecimalPipe]
})
export class AppComponent implements OnInit {
  tables$: Observable<any[]>;
  total$: Observable<number>;
  testbooks: any[] = [];

  constructor(public service: AdvancedService) {
    this.tables$ = service.jobs$;
    this.total$ = service.total$;
  }

  ngOnInit(): void {
    this.tables$.subscribe((data: any) => {
      console.log(data.length);
      this.testbooks = data;
    });
  }
}

模板代码

<div class="row">
    <div class="col-lg-12">
        <div class="card">
            <div class="card-body">
                <div class="table-responsive">
                    <table id="datatable" class="table table-bordered dt-responsive nowrap w-100 datatables">
                        <tbody *ngFor="let job of testbooks;let i=index;">
                            <tr>
                                <td>
                                    <div class="card">
                                        <div class="card-body">
                                            <h1>test</h1>
                                        </div>
                                    </div>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

错误原因

  1. API请求未缓存:fetchedJobs$是未缓存的HTTP Observable,每次_search()执行时都会重新订阅它,发起新的API请求。
  2. 状态更新触发循环:在_search()中调用this.totalRecords = jobs.length和this.endIndex = ...时,会触发对应的setter方法,而setter内的_set()会调用this._search$.next(),重新触发搜索流程,形成无限循环。
  3. 接口数据解析错误:目标API返回的是包含entries数组的对象,代码中使用Object.values(res)错误解析数据,导致后续处理异常。

修复方案

1. 缓存API请求结果

使用shareReplay(1)操作符缓存API响应,确保只发起一次请求:

// 在executions.service.ts中导入shareReplay
import { shareReplay } from 'rxjs/operators';

// 修改constructor中的fetchedJobs$初始化
this.fetchedJobs$ = this.http.get<any>('https://api.publicapis.org/entries').pipe(
  tap((res: any) => {
    this.fetchedJobs = res.entries; // 正确获取接口返回的entries数组
  }),
  shareReplay(1) // 缓存结果,避免重复请求
);

2. 避免状态更新触发循环

在_search()中直接修改_state对象,不调用setter方法,避免触发新的搜索:

private _search(): Observable<SearchResult> {
  const { pageSize, page, searchTerm } = this._state;

  return this.fetchedJobs$.pipe(
    map((res: any) => {
      let jobs = res.entries || [];

      // 过滤逻辑保持不变
      jobs = jobs.filter(job => matches(job, searchTerm, this.pipe));
      const total = jobs.length;

      // 直接计算并更新_state,不调用setter
      const totalRecords = jobs.length;
      const startIndex = (page - 1) * pageSize + 1;
      let endIndex = (page - 1) * pageSize + pageSize;
      if (endIndex > totalRecords) {
        endIndex = totalRecords;
      }
      // 直接修改_state,避免触发_search$.next()
      Object.assign(this._state, { totalRecords, startIndex, endIndex });

      jobs = jobs.slice(startIndex - 1, endIndex);

      return { jobs, total } as SearchResult;
    })
  );
}

3. 优化组件订阅(可选)

使用Angular的async管道自动管理订阅,避免手动订阅的内存泄漏:

// app.component.ts中移除testbooks和ngOnInit中的订阅
export class AppComponent {
  tables$: Observable<any[]>;
  total$: Observable<number>;

  constructor(public service: AdvancedService) {
    this.tables$ = service.jobs$;
    this.total$ = service.total$;
  }
}

修改模板:

<tbody *ngFor="let job of tables$ | async; let i=index">
  <tr>
    <td>
      <div class="card">
        <div class="card-body">
          <h1>{{ job.API }}</h1> <!-- 展示实际接口字段 -->
        </div>
      </div>
    </td>
  </tr>
</tbody>

内容的提问来源于stack exchange,提问作者Rima

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 15:20:54