Angular`ngOnInit`中的异步/等待

Posted

技术标签:

【中文标题】Angular`ngOnInit`中的异步/等待【英文标题】:async/await in 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 无济于事,我们最终会遇到两种不一致的方式(或者让新团队成员感到困惑)。 【参考方案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】:

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

【讨论】:

这已经被建议过了。请在此处查看我的答案:***.com/a/69147601/388827

以上是关于Angular`ngOnInit`中的异步/等待的主要内容,如果未能解决你的问题,请参考以下文章

等待异步函数在 Angular 中完成

在 Angular 5 中的 ngoninit 之后如何运行函数

如何从 ngOnInit Angular 中的服务获取数据

在构造函数或 ngOnInit 中加载异步函数

使用异步绑定映射与 Angular 中的组件属性的优势?

组件 ngOnInit 中的 Angular 订阅