【问题标题】:async/await in Angular `ngOnInit`Angular`ngOnInit`中的异步/等待
【发布时间】:2019-09-29 05:13:51
【问题描述】:

我目前正在评估替换 Angular 的优缺点。 RxJS 的 Observable 和普通的 Promise,这样我就可以使用 asyncawait 并获得更直观的代码风格。

我们的一个典型场景:在ngOnInit 中加载一些数据。使用Observables,我们这样做:

ngOnInit () {
  this.service.getData().subscribe(data => {
    this.data = this.modifyMyData(data);
  });
}

当我从getData() 返回Promise 并使用asyncawait 时,它变为:

async ngOnInit () {
  const data = await this.service.getData();
  this.data = this.modifyMyData(data);
}

现在,很明显,Angular 不会“知道”ngOnInit 已经变成了async。我觉得这不是问题:我的应用程序仍然像以前一样工作。但是当我查看OnInit 接口时,该函数显然没有以表明它可以声明为async 的方式声明:

ngOnInit(): void;

所以——底线:我在这里做的事情合理吗?或者我会遇到任何不可预见的问题?

【问题讨论】:

  • 根据issue 17420中的this comment:“有人使用async ngOnInit不是问题,这只是一种尴尬/不推荐的编码习惯。”
  • @ConnorsFan 在打开我的帖子之前,我实际上已经完全阅读了这个问题 :-)(应该链接它)。我仍然不确定,“尴尬”和“不推荐”是否有任何客观原因,还是 Angular 团队只是想推向响应式风格?
  • Here 是关于这个主题的另一本好书。
  • @qqilihq - 嗨,我正在考虑进行此转换。您是否继续进行并且对结果感到满意?有什么问题...?
  • @LeeGunn 这是马马虎虎。我到处使用它,但总的来说非常谨慎。原因(a)是错误处理(@Reactgular 在下面详细解释),(b)代码“嵌套”少一些(没有回调),但收益非常小,并且(c)我们正在使用代码库中有几个“真实”的 Observables(不断更新)——await 无济于事,我们最终会遇到两种不一致的方式(或者让新团队成员感到困惑)。

标签: angular typescript asynchronous promise observable


【解决方案1】:

这和你以前的没有什么不同。 ngOnInit 将返回一个 Promise,调用者将忽略该 Promise。这意味着调用者不会等待方法中的所有内容都完成后再继续。在这种特定情况下,这意味着视图将完成配置,并且可以在设置 this.data 之前启动视图。

这和你之前的情况一样。来电者不会等待您的订阅完成,并且可能会在填充 this.data 之前启动应用程序。如果您的视图依赖于data,那么您可能有某种ngIf 设置来阻止您访问它。

只要您了解其中的含义,我个人并不认为这是一种尴尬或不好的做法。但是,ngIf 可能很乏味(无论哪种方式都需要它们)。我个人已经开始使用有意义的路由解析器,这样我就可以避免这种情况。数据在路线完成导航之前加载,我可以在加载视图之前知道数据可用。

【讨论】:

  • 感谢您提及路由解析器。我认为这是解决这个经常发生的问题的更好方法。
  • "在这种特定情况下,这意味着视图将完成配置,并且可以在设置 this.data 之前启动视图。" - 这是我认为的关键,除非您考虑它,否则它并不明显,并且对于试图维护您的代码的未来开发人员来说可能更不明显。出于这个原因,我同意上面提到的评论,即这实际上是“尴尬”和“不好的做法”。
【解决方案2】:

现在,很明显,Angular 不会“知道” ngOnInit 已经变成异步的。我觉得这不是问题:我的应用仍然可以正常运行。

从语义上讲,它可以正常编译并按预期运行,但编写async / wait 的便利是以错误处理为代价的,我认为应该避免这种情况。

让我们看看会发生什么。

当一个承诺被拒绝时会发生什么:

public ngOnInit() {
    const p = new Promise((resolver, reject) => reject(-1));
}

上面生成以下堆栈跟踪:

core.js:6014 ERROR Error: Uncaught (in promise): -1
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at new ZoneAwarePromise (zone-evergreen.js:876) [angular]
    at ExampleComponent.ngOnInit (example.component.ts:44) [angular]
    .....

我们可以清楚地看到未处理的错误是由ngOnInit 触发的,还可以看到哪个源代码文件找到了有问题的代码行。

当我们使用被拒绝的async/wait 时会发生什么:

    public async ngOnInit() {
        const p = await new Promise((resolver, reject) => reject());
    }

上面生成以下堆栈跟踪:

core.js:6014 ERROR Error: Uncaught (in promise):
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at rejected (tslib.es6.js:71) [angular]
    at Object.onInvoke (core.js:39699) [angular]
    at :4200/polyfills.js:4090:36 [angular]
    at Object.onInvokeTask (core.js:39680) [angular]
    at drainMicroTaskQueue (zone-evergreen.js:559) [<root>]

发生了什么?我们不知道,因为堆栈跟踪在组件之外。

不过,您可能会想使用 Promise 并避免使用 async / wait。因此,让我们看看如果在 setTimeout() 之后拒绝 promise 会发生什么。

    public ngOnInit() {
        const p = new Promise((resolver, reject) => {
            setTimeout(() => reject(), 1000);
        });
    }

我们将得到以下堆栈跟踪:

core.js:6014 ERROR Error: Uncaught (in promise): [object Undefined]
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at :4200/app-module.js:21450:30 [angular]
    at Object.onInvokeTask (core.js:39680) [angular]
    at timer (zone-evergreen.js:2650) [<root>]

再一次,我们在这里失去了上下文,不知道去哪里修复错误。

Observables 遭受错误处理的相同副作用,但通常错误消息的质量更好。如果有人使用throwError(new Error())Error 对象将包含堆栈跟踪,如果您使用HttpModuleError 对象通常是 Http 响应 告诉你请求的对象。

所以这里的故事的寓意是:抓住你的错误,尽可能使用 observables,不要使用async ngOnInit(),因为它会作为一个难以找到和修复的错误回来困扰你。

【讨论】:

  • 很好的解释。
  • 这里缺少的部分是当Observable 中发生错误时堆栈跟踪的外观会更好吗?
  • @Peter 大多数抛出错误的 observables 通常来自HttpClient 并且是异步的。因此,当抛出错误时,堆栈跟踪将来自您的源代码之外。您可以使用catchError() 操作符来处理错误,如果您只需要堆栈跟踪帮助,那么catchError(err =&gt; throwError(err)) 将重新抛出错误并将您的源代码添加到调用堆栈中。
【解决方案3】:

我想知道immediately invoked function expression 的缺点是什么:

    ngOnInit () {
      (async () => {
        const data = await this.service.getData();
        this.data = this.modifyMyData(data);
      })();
    }

这是我能想到的唯一方法,无需将 ngOnInit() 声明为 async 函数即可使其工作

【讨论】:

  • 但这与拥有一个单独的异步函数并在 ngOnInit ngOnInit () { this.fetchData(); 中调用它一样好} private async fetchData(){ const data = await this.service.getData(); this.data = this.modifyMyData(data); }
  • @ABajpai 是的,但是没有单独的async function 也没有创​​建ngOninit() async,这似乎是用户要求的。
  • 这和只运行promise而不等待它是一样的,它会工作,但不等待它完成
【解决方案4】:

我在 ngOnInit() 中使用了 try catch:

async ngOnInit() {      
   try {           
       const user = await userService.getUser();
    } catch (error) {           
        console.error(error);       
    }    
} 

然后你会得到一个更具描述性的错误,你可以找到错误所在

【讨论】:

    【解决方案5】:

    你可以使用rxjs函数of

    of(this.service.getData());
    

    将承诺转换为可观察序列。

    【讨论】:

    • 谢谢。但我的目标是避免 Observable 支持 await 语法(在过去,我对基于回调的语法产生了强烈的反感:-))。
    • @qqilihq 我同意,Promise 很丑,然后我们得到了awaitasync,我很高兴,现在我们有了Observable,我们又回到了丑陋但比Promise.
    【解决方案6】:

    ngOnInitdoes NOT wait 承诺完成。如果您想像这样使用 await,可以将其设为异步函数:

    import { take } from 'rxjs/operators';
    
    async ngOnInit(): Promise<any> {
      const data = await this.service.getData().pipe(take(1)).toPromise();
      this.data = this.modifyMyData(data);
    }
    

    但是,如果您使用 ngOnInit 而不是构造函数来等待函数完成,那么您基本上是在做与此等效的操作:

    import { take } from 'rxjs/operators';
    
    constructor() {
      this.service.getData().pipe(take(1)).toPromise()
        .then((data => {;
          this.data = this.modifyMyData(data);
        });
    }
    

    它将运行异步函数,但不会等待它完成。如果您注意到有时它会完成,有时它不会,这实际上取决于您的函数的时间。

    利用this post的思路,基本可以跑到zone.js之外了。 NgZone 不包括 scheduleMacroTask,但 zone.js 已经导入到 angular 中。

    解决方案

    import { isObservable, Observable } from 'rxjs';
    import { take } from 'rxjs/operators';
    
    declare const Zone: any;
    
    async waitFor(prom: Promise<any> | Observable<any>): Promise<any> {
      if (isObservable(prom)) {
        prom = prom.pipe(take(1)).toPromise();
      }
      const macroTask = Zone.current
        .scheduleMacroTask(
          `WAITFOR-${Math.random()}`,
          () => { },
          {},
          () => { }
        );
      return prom.then((p: any) => {
        macroTask.invoke();
        return p;
      });
    }
    

    我个人把这个函数放在了我的core.module.ts,虽然你可以把它放在任何地方。

    像这样使用它:

    constructor(private cm: CoreModule) { 
      const p = this.service.getData();
      this.post = this.cm.waitFor(p);
    }
    

    您还可以检查 isBrowser 以保持可观察性,或等待结果。

    相反,您也可以导入 angular-zen 并像在 this post 中一样使用它,尽管您导入的内容会超出您的需要。

    J

    【讨论】:

    • async ngOnInit(): Promise 对我有用。
    【解决方案7】:

    我会做一些不同的事情,你不显示你的 html 模板,但我假设你正在用数据做一些事情,即

    <p> {{ data.Name }}</p> <!-- or whatever -->
    

    您不使用异步管道是否有原因?即

    <p> {{ (data$ | async).Name }}</p>
    

    <p *ngIf="(data$ | async) as data"> {{ data.name }} </p>
    

    在你的ngOnInit:

    data$: Observable<any>; //change to your data structure
    ngOnInit () {
      this.data$ = this.service.getData().pipe(
        map(data => this.modifyMyData(data))
      );
    }
    

    【讨论】:

      【解决方案8】:

      这个问题的正确答案是使用 Angular 的Resolver 特性。使用 Resolve,您的组件在您告诉它渲染之前不会被渲染。

      来自文档:

      类可以实现为数据提供者的接口。一个数据 提供者类可以与路由器一起使用来解析数据 导航。该接口定义了一个调用的 resolve() 方法 当导航开始时。路由器等待数据 在路由最终激活之前解决。

      【讨论】:

      • 解析器是一个需要考虑的概念,但它们不是灵丹妙药。在某些情况下,onInit 是可取的。 (例如,更少的代码分散,在目标视图中显示加载指示器,......)
      【解决方案9】:

      说白了,你的所作所为是不合理。 如果您在导航到该页面之前有哪些可用数据,请在您的路线中使用解析器

      【讨论】:

      猜你喜欢
      • 2020-12-13
      • 1970-01-01
      • 2019-11-05
      • 1970-01-01
      • 2017-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多