【问题标题】:Asynchronous data returning in random order以随机顺序返回的异步数据
【发布时间】:2022-02-01 05:49:40
【问题描述】:

我正在使用 pokeapi 获取 151 个 pokemon 的数据。由于 API 没有获取所有 pokemon 的路由,因此我必须发送 151 个异步 get 请求(每个 pokemon 一个)。

尽管按顺序查询数据,但我注意到口袋妖怪是按随机顺序返回的,每次都以不同的顺序返回。我可以使用哪些策略来防止这种情况并按照请求的顺序排列它们(#1 - #151)?我假设这是由于请求的异步性质。 Async/await 可能有用,但我无法在这里成功实现。

pokedex.component.html:

<div class="container">
    <div *ngFor="let pokemon of pokemons" class="pokemon-card">
        <div>
            <img src="{{ pokemon.sprites.front_default }}"/>
        </div>
        <span style="display:flex;">#{{pokemon.id}}</span>
        <span>{{pokemon.name | titlecase}}</span>
    </div>
</div>

pokedex.component.ts:

  constructor(public _httpService: HttpService) { }

  ngOnInit(): void {
     this.getPokemon();
  }

  pokemons = [];

  getPokemon() {
    this._httpService.getPokemon().subscribe( async (data) => {
      data.results.forEach((pokemon)=>{this.getPokemonDetails(pokemon.name)});
    });
  }

  getPokemonDetails(pokemon) {
    this._httpService.getPokemonDetails(pokemon).subscribe((data)=>{
      this.pokemons.push(data);
    });
  }

http.service.ts:

constructor(private http: HttpClient) { }

pokemonsUrl = 'https://pokeapi.co/api/v2/pokemon';

  getPokemon(): Observable<any> {
    return this.http.get(`${this.pokemonsUrl}?limit=151`);
  }

  getPokemonDetails(name): Observable<any> {
    return this.http.get(`${this.pokemonsUrl}/${name}`);
  }

【问题讨论】:

  • 试试这个:this.pokemons[data.id - 1] = data;(而不是推送)
  • 上面的注释似乎是最简单的,只要记住处理ngFor中未定义的情况即可。
  • @ChrisG 这似乎可以解决问题,尽管现在我得到了像ERROR TypeError: Cannot read properties of undefined (reading 'sprites') 这样的控制台错误,即使精灵加载正确。为什么.push() 以随机顺序构建数组,而使用array[data.id - 1] 却保持正确顺序?
  • 问题在这里:data.results.forEach((pokemon)=&gt;{this.getPokemonDetails(pokemon.name)});你可以做的是使用“for of”语句并在其中等待。但这意味着每次只有一个请求。或者您记住索引并将其发送到 getPokemonDetails 以获取列表的正确索引。不过,您的编码方式可能不是最好的。尝试只使用一个订阅...请查看 switchMap、merge、combine ... 等。
  • 您正在并行运行所有请求,因此它们将以任意顺序完成。这意味着您以任意顺序将它们推入数组。当您改用我的行时,每个对象都放在正确的索引处。这意味着该数组还将在其间获得一堆 undefined 元素,直到各自的请求完成。正确的解决方案是使用Promise.all

标签: javascript angular typescript asynchronous rxjs


【解决方案1】:

您可以在订阅中跳过async 的使用,并使用RxJS higher order mapping operatorswitchMapforkJoin 函数来并行触发多个请求。

保证输出的顺序是输入的observables的顺序。

constructor(public _httpService: HttpService) { }

ngOnInit(): void {
  this.getPokemon();
}

pokemons = [];

getPokemon() {
  this._httpService.getPokemon().pipe(
    switchMap((data) => 
      forkJoin(
        data.results.map((pokemon: any) => 
          this._httpService.getPokemonDetails(pokemon)
        )
      )
    )
  ).subscribe({
    next: (results: any) => this.pokemons = results,
    error: (error: any) => {
      // handle error
    }
  })
}

话虽如此,请注意对来自浏览器的最大并行请求数的域特定限制。如果您觉得并行请求会减慢您的应用程序,请参考 here 以获取替代方案。


更新

我为Stackblitz 中的代码创建了一个工作示例,并注意到getPokemonDetails() 方法中存在错误。

目前您似乎在 URL 中传递整个对象。这将导致错误。此外,每个对象在响应中都包含它对应的 URL。明智的做法是使用它,这样以后对 API 的任何更改都不会影响您的应用程序。

我做了以下更改:

  1. 由于我们订阅 observable 只是为了使用模板中的排放,您可以替换控制器中的订阅 (.ts) 并在模板中使用 async 管道 (.html )。

  2. 将 URL 发送到 getPokemonDetails() 以获取信息。

组件控制器 (*.ts)

export class AppComponent {
  pokemons$: Observable<any>;

  constructor(public _httpService: HttpService) {}

  ngOnInit(): void {
    this.getPokemon();
  }

  getPokemon() {
    this.pokemons$ = this._httpService
      .getPokemon()
      .pipe(
        switchMap((data: any) =>
          forkJoin(
            data.results.map((pokemon: any) =>
              this._httpService.getPokemonDetails(pokemon.url)
            )
          )
        )
      );
  }
}

组件模板 (*.html)

<div class="container">
  <div *ngFor="let pokemon of pokemons$ | async" class="pokemon-card">
    <div>
      <img src="{{ pokemon.sprites.front_default }}" />
    </div>
    <span style="display:flex;">#{{ pokemon.id }}</span>
    <span>{{ pokemon.name | titlecase }}</span>
  </div>
</div>

服务

getPokemonDetails(url): Observable<any> {
  return this.http.get(url);
}

工作示例:Stackblitz

【讨论】:

    【解决方案2】:

    既然您提到了async / await,我也会向您展示如何实现它。你不能单独 await 一个 observable,但你可以将 observable 包装在 promise 中。

    我会像这样重做 http.service.ts

    import { HttpClient } from '@angular/common/http';
    import { Injectable } from '@angular/core';
    
    @Injectable({
      providedIn: 'root',
    })
    export class HttpService {
      constructor(private http: HttpClient) {}
    
      pokemonsUrl = 'https://pokeapi.co/api/v2/pokemon';
    
      getPokemon(): Promise<any> {
        return new Promise((resolve) =>
          this.http
            .get(`${this.pokemonsUrl}?limit=151`)
            .subscribe((data: any) => resolve(data.results))
        );
      }
    
      getPokemonDetails(name: string): Promise<any> {
        return new Promise((resolve) =>
          this.http
            .get(`${this.pokemonsUrl}/${name}`)
            .subscribe((data) => resolve(data))
        );
      }
    }
    

    那么组件就变得简单多了

      constructor(public _httpService: HttpService) {}
    
      ngOnInit(): void {
        this.getPokemon();
      }
    
      pokemons: any[] = [];
    
      async getPokemon() {
        const pokemon: any[] = await this._httpService.getPokemon();
        for (const p of pokemon) {
          this.pokemons.push(await this._httpService.getPokemonDetails(p.name));
        }
      }
    

    但这有点慢,因为我们是一个接一个地调用 api。为了加快速度,您可以将 Promise 放在一个数组中,并同时使用 Promise.allawait

      async getPokemon() {
        const pokemon: any[] = await this._httpService.getPokemon();
        const promises = [];
        for (const p of pokemon) {
          promises.push(this._httpService.getPokemonDetails(p.name));
        }
        this.pokemons = await Promise.all(promises);
      }
    

    【讨论】:

      【解决方案3】:

      一个简单的解决方案是使用forEachindex 参数

        constructor(public _httpService: HttpService) { }
      
        ngOnInit(): void {
           this.getPokemon();
        }
        
        pokemons = [];
      
        getPokemon() {
          this._httpService.getPokemon().subscribe((data) => {
            data.results.forEach((pokemon, index)=>{this.getPokemonDetails(pokemon.name, index)});
          });
        }
      
        getPokemonDetails(pokemon, index) {
          this._httpService.getPokemonDetails(pokemon).subscribe((data)=>{
            this.pokemons[index] = data;
          });
        }
      

      您只需要确保您没有尝试访问 html 中的空槽

      <div class="container">
        <ng-container *ngFor="let pokemon of pokemons">
          <div *ngIf="pokemon" class="pokemon-card">
            <div>
              <img src="{{ pokemon.sprites.front_default }}" />
            </div>
            <span style="display: flex">#{{ pokemon.id }}</span>
            <span>{{ pokemon.name }}</span>
          </div>
        </ng-container>
      </div>
      

      为我工作。我还添加了一个 async / await 解决方案作为单独的答案,它不会在数组中创建空槽。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-09-15
        • 2011-03-28
        • 2019-02-01
        • 2010-11-10
        • 2017-12-07
        • 2021-04-12
        • 1970-01-01
        • 2018-11-28
        相关资源
        最近更新 更多