如何组合可观察函数的多次执行?

时间:2021-03-24 10:32:37

标签: angular typescript firebase rxjs angularfire2

在 firestore 中,我想检索多个文档。每个文档都有一个唯一的 sourceAddressValue,因此对于 N 个字符串的列表,我想检索潜在的 N 个文档。

我尝试执行以下操作:

getLocationAddresses(addresses: string[]) {
  const chunkSize = 10;
  let addressesChunks = [];
  if (addresses.length < chunkSize) {
      addressesChunks.push(addresses);
  } else {
    addressesChunks = [...Array(Math.ceil(addresses.length / chunkSize))].map(_ => addresses.splice(0,chunkSize));
  }
  console.log(addressesChunks);
  return of(...addressesChunks).pipe(
    mergeMap<string[], any>((x) => this.db.collection('locations', ref => 
    ref.where('sourceLocation', 'array-contains', x)).valueChanges()),
    toArray() // when this is removed, the code inside getOrders is triggered multiple times
);
  }

public getOrders() {
        this.getJSON().subscribe(data => {
            this.orders = data.orders;
            const addresses = this.orders.map(item => `${item.address}, ${item.postalCode}`);
            this.dbService.getLocationAddresses(addresses).subscribe(data => {
                console.log('data retrieved');
                console.log(data);
            });
            this.ordersRefreshed.next(this.orders);
        });
    }

在尝试执行上面的代码时,似乎执行没有完成。然而,当我在 getLocationAddresses 中注释掉 toArray() 时,订阅的函数会被多次触发,分别针对每个块。

有谁知道如何将 observable 函数的多个完成分组,以便它只触发观察者一次?

3 个答案:

答案 0 :(得分:2)

让我们先解释一下您看到的行为:

<块引用>

然而,当我在 toArray() 中注释掉 getLocationAddresses 时,订阅的函数会被多次触发,分别针对每个块。

每次收到发射时都会触发 subscribe 中的代码。当您使用 mergeMap 时,您正在创建一个具有多个“内部 observables”的 observable。每当这些内部 observable 中的任何一个发出时,mergeMap 创建的 observable 都会发出。

因此,如果您将 n 次排放量传递给 mergeMap,您可以预期至少 n 次排放量(这些内部 observable 可能发出不止一个时间).

<块引用>

[with toArray] 好像执行没有完成。

当你使用 toArray() 时,它不会允许任何排放,直到它的源可观察完成;然后它发出所有接收到的发射的数组。在这种情况下,源是由 mergeMap 创建的 observable,它由多个 .valueChanges() observable 组成。

然而,firestore .valueChanges() 创建的 observable 会在返回集合中的任何文档发生更改时发出,但永远不会完成。由于这些 observable 是长期存在的,toArray() 永远不会发出任何东西。

This StackBlitz 说明了这个问题。

解决方案

解决方案取决于您想要的行为。您是否打算调用这些查询中的每一个并返回结果(一个和完成),或者您打算维护一个反应流,以发出查询的最新表示?

一个就完成

您可以使用 take(1) 强制 observable 在收到 1 次发射后完成,从而允许 toArray() 也完成 (Example - 1A):

return of(...addressesChunks).pipe(
  mergeMap(x => 
    this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'array-contains', x)
    ).valueChanges().pipe(take(1))  // <-- take(1) forces completion of inner observables
  ),
  toArray()
);

您可以使用 of (Example 1B) 代替 mergeMap/toArray/forkJoin

return forkJoin(
  addressesChunks.map(
    x => this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'array-contains', x)
    ).valueChanges().pipe(take(1))
  )
);

反应式可观察

您可以使用 combineLatest 从多个源创建一个 observable,该 observable 在任何一个源发出时都会发出:

return combineLatest(
  addressesChunks.map(
    x => this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'array-contains', x)
    ).valueChanges()
  )
);

不过,我相信 Firestore .valueChages() 已经为您做的差不多了。我知道您正在对查询进行分块,但我很好奇为什么。

看起来您发出多个查询只是为了在获得返回值时将结果重新组合在一起。

我相信您可以简单地将所有 addresses 传递给一个

ref.where('sourceLocation', 'array-contains', addresses)

立即调用并获取结果。这样做对性能有影响吗?

答案 1 :(得分:0)

通过使用 combineLatest,函数 getLocationAddresses() 返回合并结果:

getLocationAddresses(addresses: string[]) {
      const chunkSize = 10;
      let addressesChunks: string[][] = [];
      if (addresses.length < chunkSize) {
          addressesChunks.push(addresses);
      } else {
        addressesChunks = [...Array(Math.ceil(addresses.length / chunkSize))].map(_ => addresses.splice(0,chunkSize));
      }
      console.log(addressesChunks);
      const observables = addressesChunks.map(addresses => this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'in', addresses)).valueChanges());
      return combineLatest(observables)
      .pipe(map(arr => arr.reduce((acc, cur) => acc.concat(cur) ) ),);
  }

答案 2 :(得分:0)

我认为这里的关键点是 valueChanges() 返回一个 Observable,它会在它引用的文档发生变化时通知任何时间。这基本上意味着返回的 Observable 没有完成,因此通过 mergeMap 生成的流没有完成,因此 toArray 不会触发,因为它会在源流完成时触发。

另一方面,

combineLatest 会在作为参数传入的 observable 中的任何一个在至少触发一次之后触发。

现在,如果只想检索文档并且不想让 Observable 在此类文档发生变化时发出任何消息,那么您可以尝试将 take(1) 运算符通过管道传输到每个 mergeMap。< /p>

代码看起来像这样

getLocationAddresses(addresses: string[]) {
  const chunkSize = 10;
  let addressesChunks = [];
  if (addresses.length < chunkSize) {
      addressesChunks.push(addresses);
  } else {
    addressesChunks = [...Array(Math.ceil(addresses.length / chunkSize))].map(_ => addresses.splice(0,chunkSize));
  }
  console.log(addressesChunks);
  return of(...addressesChunks).pipe(
    mergeMap<string[], any>((x) => this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'array-contains', x)).valueChanges().pipe(
         take(1)
      )
    ),
    toArray() // when this is removed, the code inside getOrders is triggered multiple times
);
}

take(1) 触发第一个通知,然后完成 Observable。由于上游 observable 最终会完成,因此 toArray 可以触发。

我不熟悉 Firebase rxJs 库,也许有些方法/函数包含我在上面尝试描述的 take(1) 行为,但我希望我能够传递核心概念。