使用Observable和async/await结合使用是一个好的实践吗?

91

我正在使用Angular 2通用的http返回Observable,但是当我使用嵌套Observable调用时,我的代码会变得混乱无序:

this.serviceA.get().subscribe((res1: any) => {
   this.serviceB.get(res1).subscribe((res2: any) => {
       this.serviceC.get(res2).subscribe((res3: any) => {

       })
   })
})

现在我想使用async/await来避免这个问题,但是async/await只能与Promise一起使用。我知道Observable可以转换为Promise,但据我所知,这不是一个好的做法。那么我应该在这里怎么做呢?

顺便说一句,如果有人能给我一个用async/await解决这个问题的示例代码就太好了:D


如果您没有使用任何可观察对象的功能,那么您可能可以逃脱。至于它是否是良好的实践,可能不是这样。 - Titian Cernicova-Dragomir
11
然而,嵌套订阅是不良实践:它会导致回调地狱,而 Promise 和 Observable 本来应该帮助我们避免这种情况。 - Pac0
7
如果你想要链接多个Observables,可以使用flatMap / switchMap运算符。 this.serviceA.get().flatMap((res1: any) => this.serviceB.get()).flatMap((res2: any) => this.serviceC.get()).subscribe( (res3: any) => { .... }); - Pac0
1
我对 Promises 和 observables 的看法是它们是实现不同目标的工具,而不是彼此替代的选择。例如,Promises 适用于处理需要执行的一组操作,observables 适用于需要通知多个订阅者的情况。当然,在适当的情况下混合使用它们是完全有意义的。 - Keith
在我看来,对于一次性操作和单个订阅者,不应该使用可观察对象。这正是承诺与async/await所用的(而且它们更易读)。 - spyro
4个回答

110

在您的代码中按顺序链接Observables

关于您的代码示例,如果您想链接Observables(在前一个发出后触发另一个),请使用flatMap(或switchMap)来实现此目的:

this.serviceA.get()
  .flatMap((res1: any) => this.serviceB.get())
  .flatMap((res2: any) => this.serviceC.get())
  .subscribe( (res3: any) => { 
    .... 
  });

与嵌套相比,这种做法更好,因为它会使问题更加清晰,并帮助您避免回调地狱,这也是 Observable 和 Promises 一开始就应该帮助防止的。

此外,请考虑使用 switchMap 而不是 flatMap,基本上它将允许在第一个请求发出新值时“取消”其他请求。例如,如果第一个触发其余请求的 Observable 是某个按钮的点击事件,则可以使用此选项。

如果您不需要各种请求依次等待彼此,可以使用 forkJoinzip 同时启动它们,有关详细信息和其他见解,请参见 @Dan Macak answer's


Angular 'async' 管道和 Observables 很好地协同工作

关于 Observables 和 Angular,您可以完全在 Angular 模板中使用 | async 管道,而不是在组件代码中订阅 Observable,以获取由此 Observable 发出的值。


ES6 async / await 和 Promise 而不是 Observables?

如果你不想直接使用 Observable,你可以简单地在你的 Observable 上使用 .toPromise(),然后一些 async/await 指令。

如果您的 Observable 只返回一个结果(正如基本 API 调用的情况),则可以将 Observable 视为与 Promise 相当。

但是,考虑到 Observable 已经提供了所有东西 (向读者说明:欢迎提供启示性的反例!)。我更倾向于尽可能使用 Observables 作为训练练习。


一些有趣的博客文章(还有很多其他文章):

https://medium.com/@benlesh/rxjs-observable-interop-with-promises-and-async-await-bebb05306875

toPromise 函数实际上有点棘手,因为它不是真正的“操作符”,而是 RxJS 特定的一种方式,订阅 Observable 并将其包装在一个 Promise 中。一旦 Observable 完成,Promise 将解析为 Observable 的最后发出的值。

这意味着,如果 Observable 发出值“hi”,然后等待 10 秒钟才完成,则返回的 promise 将等待 10 秒钟才解析“hi”。如果 Observable 永远不完成,则 Promise 永远不会解析。

注意:除非处理期望 Promise 的 API(例如 async-await)的情况,否则使用 toPromise()是一种反模式。

(强调是我自己加的)


您请求的示例

顺便说一句,如果有人能给我一个用 async/await 来解决这个问题的示例代码,那就太好了:D

如果你真的想这样做,那么以下是一个示例代码(可能会有些错误,无法检查,请随意更正)

// Warning, probable anti-pattern below
async myFunction() {
    const res1 = await this.serviceA.get().toPromise();
    const res2 = await this.serviceB.get().toPromise();
    const res3 = await this.serviceC.get().toPromise();
    // other stuff with results
}
在您可以同时启动所有请求的情况下,await Promise.all()应该更有效,因为这些调用之间互不依赖。(就像Observables会使用forkJoin一样)
async myFunction() {
    const promise1 = this.serviceA.get().toPromise();
    const promise2 = this.serviceB.get().toPromise();
    const promise3 = this.serviceC.get().toPromise();

    let res = await Promise.all([promise1, promise2, promise3]);

    // here you can retrieve promises results,
    // in res[0], res[1], res[2] respectively.
}

2
如果有人能添加Observable zip和forkJoin的示例,那将非常好。当请求可以并行进行时,例如嵌套订阅不需要来自前一个请求的数据时,可以使用它们。 - Vayrex
2
forkJoinzip都是不错的想法,前者甚至更好,因为它仅发出内部Observables的最后一个值。我将在答案中详细说明。 - Dan Macak
@DanMacák 我同意在这个例子中,请求不需要互相等待,因为没有一个依赖于前面的结果。然而,我想给出一个例子,其中一个人可以实际上使用先前的结果,这可能是情况(你得到一些对象信息,有一些ID,你做另一个调用到另一个API等等)。根据嵌套订阅,OP 可以在第二个调用中使用res1,res2在第二个调用中等等... 我不想破坏这个潜在的用法。 - Pac0
@Pac0 很高兴能帮忙,尤其是在这样一个有趣的话题上。 - Dan Macak
1
@Pac0,恭喜你的出色回答。我只想补充一下你最后一个示例中如何从Promise.all中检索值,使用let res = await Promise.all(...),然后分别通过res[0] / res[1] / res[2]获取。 - Jeto
显示剩余6条评论

33

正如@Pac0已经很好地解释了各种解决方案,我只想从稍微不同的角度补充一点。

混合使用Promises和Observables

个人而言,我更喜欢不要混用Promises和Observables - 使用async/await和Observables时就会出现这种情况,因为尽管它们看起来相似,但它们是非常不同的。

  • Promises始终是异步的,而Observables不一定是
  • Promises仅表示1个值,Observables则可以表示0、1或多个值
  • Promises的用途非常有限,例如您无法取消它们(暂且放下ES提案),而Observables在使用上更加强大(您可以使用它们管理多个WS连接,试试用Promises实现这一点)
  • 它们的API有很大的区别

在Angular中使用Promises

尽管有时候同时使用两者是有效的,特别是在Angular中,我认为应该尽可能地使用RxJS。原因如下:

  • Angular API的很大一部分使用Observables(路由器、http等),因此使用RxJS一种是顺着潮流(不是故意的)而不是反着来。否则,您将不得不一直转换为Promises,以弥补RxJS提供的丢失功能
  • Angular拥有强大的async管道,可以将整个应用程序的数据流组合成流,可以对其进行过滤、组合和任何修改,而不会中断从服务器传递的数据流,而无需进行then或subscribe的任何操作。这样,您不需要解包数据或将其分配给一些辅助变量,数据只需从服务通过Observables直接流到模板中,非常美妙。

但也存在某些情况下Promises仍然可以发挥作用。例如,在typescript中缺少单个概念。如果您正在创建API供其他人使用,则返回Observable并不都能清楚地告诉调用者:您将获得1个值、多个值还是仅完成?您必须编写注释来解释它。另一方面,Promise在这种情况下具有更清晰的契约。它始终将解析一个值或拒绝一个错误(除非它永远挂起)。

总的来说,您肯定不需要在项目中只使用Promises或只使用Observables。如果您只想表示某个事物“已完成”的价值,而不想将其集成到某个流程中,则Promise更自然。另外,使用async/await使您可以以顺序方式编写代码,从而大大简化了它,因此,除非需要对传入的值进行高级管理,否则可以使用Promise。


回到您的示例

因此,我的建议是充分利用RxJS和Angular的功能。回到您的示例,您可以编写以下代码(感谢@Vayrex的创意):

this.result$ = Observable.forkJoin(
  this.serviceA.get(),
  this.serviceB.get(),
  this.serviceC.get()
);

this.result$.subscribe(([resA, resB, resC]) => ...)
这段代码将会发出3个请求,一旦这三个请求的Observables都完成了,forkJoin订阅回调函数将把结果放在一个数组中返回给你。正如所说,你可以像示例中那样手动订阅它,或者在模板中使用result$async管道来声明性地执行这个操作。
在这里使用Observable.zip也会得到相同的结果,不同之处在于前者仅发出内部Observables的最后一个值,而后者组合内部Observables的第一个值、第二个值等等。
编辑:由于您需要先前HTTP请求的结果,请使用@Pac0答案中的flatMap方法。

1
实际上,我必须在serviceB.get()调用中使用resA。使用Observable的最佳方法是什么? - Thach Huynh
这是flatMap方法,就像Pac0s示例中的第一个片段一样。 - Dan Macak
3
subscribe 的第二个参数是错误回调函数,如果你不想显式订阅或者想要更多的自由来处理错误,你可以在 forkJoin 后面调用 catch 方法并将错误处理函数作为第一个参数传入。在这里,你可以将错误映射到其他值上。 - Dan Macak
source$.switchMap(x => promiseDelay(x)) // 可以执行 .subscribe(x => console.log(x)); - Ashwin J

11

由于 toPromise 已在 2022 年被弃用,我想展示使用另一种方法在 observable 上使用 await 的方式。与复杂的 rxjs 管道相比,我发现这种方法使代码更易读。这对于 http 请求尤其有用,因为只有一个响应,并且通常希望在执行其他操作之前等待响应。


更新

我的初始解决方案可行,但是 rxjs 基本上具有相同的功能:firstValueFrom()

从文档中得知:

async function execute() {
  const source$ = interval(2000);
  const firstNumber = await firstValueFrom(source$);
  console.log(`The first number is ${firstNumber}`);
}

原始解决方案

如果您有一个可观察对象,您可以将其包装在一个Promise中,订阅并在订阅发出时解析它。


getSomething(): Promise<any> {
    return new Promise((resolve, reject) => {
      this.http
        .get('www.myApi.com')
        .subscribe({
          next: (data) => resolve(data),
          error: (err) => reject(err),
        });
    });
  }

现在我们可以在async函数中等待响应。

  async ngOnInit() {
    const data = await this.getSomething();
    //Do something with your data
  }

现在我们可以对数据执行大量复杂操作,这将使那些不是rxjs专家的人更容易阅读。如果你有三个相继的HTTP请求彼此依赖,它会像这样:

  async ngOnInit() {
    const first = await this.getFirst();
    const second = await this.getSecond(first);
    const third = await this.getThird(second);
  }

7

Observables很适用于流式处理,例如BehaviorSubject。但是对于单个数据请求(例如http.get()),最好将服务调用本身设置为异步。

async getSomethingById(id: number): Promise<Something> {
    return await this.http.get<Something>(`api/things/${id}`).toPromise();
}

然后,您可以像这样简单地调用它:

async someFunc(): Promise {
    console.log(await getSomethingById(1));
}

RxJS非常强大,但在处理一个简单的API调用时似乎有点杀鸡焉用牛刀。即使您需要操纵获取到的数据,您仍然可以在getSomethingById函数中使用RxJS运算符,并返回最终结果。

使用async/await的明显优势是它更易于阅读,且不需要费力地链接调用。


即使流也比承诺更好。使用AsyncEnumerable并将它们实现为生成器函数。 - justin.m.chase
@Justin,用C#可以的。你能用TS/JS做吗? - Glaucus
2
当然可以。你可以创建异步生成器函数并返回 AsyncEnumerables,然后可以使用 for await (const i of items()) 等方式进行操作。 - justin.m.chase

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接