Javascript Angular 7-承诺内的每次迭代在承诺解析后执行。为什么?

Javascript Angular 7-承诺内的每次迭代在承诺解析后执行。为什么?,javascript,angular,typescript,promise,Javascript,Angular,Typescript,Promise,我为调用drawPoll()函数之前需要进行的一些操作创建了一个服务。我添加了控制台日志来跟踪执行顺序,但无法理解链接到.then()的函数为何在promise内部的forEach迭代完成之前执行。创建服务并将forEach操作包装到promise中的整个要点是,这样我就可以完全确定forEach迭代在调用drawPoll()函数之前已经完成。我错过了什么 poll.component.ts import { Component, OnInit, Input, Output, EventEmit

我为调用drawPoll()函数之前需要进行的一些操作创建了一个服务。我添加了控制台日志来跟踪执行顺序,但无法理解链接到.then()的函数为何在promise内部的forEach迭代完成之前执行。创建服务并将forEach操作包装到promise中的整个要点是,这样我就可以完全确定forEach迭代在调用drawPoll()函数之前已经完成。我错过了什么

poll.component.ts

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import * as Chart from 'chart.js';
import { Observable } from 'rxjs';
import { FirebaseService } from '../services/firebase.service';
import { first } from 'rxjs/operators';
import { CardModule } from 'primeng/card';
import { AngularFireAuth } from '@angular/fire/auth';

import nflPollTypes from '../../assets/types/poll-types-nfl.json';
import nflScoringTypes from '../../assets/types/scoring-types-nfl.json';

@Component({
  selector: 'app-poll',
  templateUrl: './poll.component.html',
  styleUrls: ['./poll.component.scss']
})
export class PollComponent implements OnInit {
  chart:any;
  poll:any;
  votes:[] = [];
  labels:string[] = [];
  title:string = "";
  isDrawn:boolean = false;
  inputChoices:any = [];
  username:string = "";
  points:number;
  uid:string = "";
  votedChoice:string;
  hasVoted:boolean = false;
  scoringTypeString:string;
  nflPollTypes:any = nflPollTypes.types;
  nflScoringTypes:any = nflScoringTypes.types;

  @Input()
  pollKey: string;

  @Input()
  pollDocument:any;

  @Output()
  editEvent = new EventEmitter<string>();

  @Output()
  deleteEvent = new EventEmitter<string>();

  constructor(private firebaseService: FirebaseService, private afAuth: AngularFireAuth) { }

  ngOnInit() {
    const pollData:any = this.pollDocument.payload.doc;
    this.pollKey = pollData.id;
    this.poll = {
      id: this.pollKey,
      helperText: pollData.get("helperText"),
      pollType: pollData.get("pollType"),
      scoringType: pollData.get("scoringType"),
      user: pollData.get("user")
    };

    this.firebaseService.initPoll(this.pollKey, this.isDrawn, this.drawPoll).then((choices, votedChoice) => {
      this.poll.choices = choices;
      this.votedChoice = votedChoice;
      this.drawPoll();
    })
  }

  drawPoll() {
    console.log("DRAW!", this.poll);
    if (this.isDrawn) {
      this.chart.data.datasets[0].data = this.poll.choices.map(choice => choice.votes);
      this.chart.data.datasets[0].label = this.poll.choices.map(choice => choice.text);
      this.chart.update()
    }
    if (!this.isDrawn) {
      this.inputChoices = this.poll.choices;
      var canvas =  <HTMLCanvasElement> document.getElementById(this.pollKey);
      if(canvas) {
        var ctx = canvas.getContext("2d");
        this.chart = new Chart(ctx, {
          type: 'horizontalBar',
          data: {
            labels: this.poll.choices.map(choice => choice.text),
            datasets: [{
              label: this.title,
              data: this.poll.choices.map(choice => choice.votes),
              fill: false,
              backgroundColor: [
                "rgba(255, 4, 40, 0.2)",
                "rgba(19, 32, 98, 0.2)",
                "rgba(255, 4, 40, 0.2)",
                "rgba(19, 32, 98, 0.2)",
                "rgba(255, 4, 40, 0.2)",
                "rgba(19, 32, 98, 0.2)"
              ],
              borderColor: [
                "rgb(255, 4, 40)",
                "rgb(19, 32, 98)",
                "rgb(255, 4, 40)",
                "rgb(19, 32, 98)",
                "rgb(255, 4, 40)",
                "rgb(19, 32, 98)",
              ],
              borderWidth: 1
            }]
          },
          options: {
            events: ["touchend", "click", "mouseout"],
            onClick: function(e) {
              console.log("clicked!", e);
            },
            tooltips: {
              enabled: true
            },
            title: {
              display: true,
              text: this.title,
              fontSize: 14,
              fontColor: '#666'
            },
            legend: {
              display: false
            },
            maintainAspectRatio: true,
            responsive: true,
            scales: {
              xAxes: [{
                ticks: {
                  beginAtZero: true,
                  precision: 0
                }
              }]
            }
          }
        });
        this.isDrawn = true;
      }
    }
  }

}
import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
import { map, switchMap, first } from 'rxjs/operators';
import { Observable, from } from 'rxjs';
import * as firebase from 'firebase';
import { AngularFireAuth } from '@angular/fire/auth';

@Injectable({
  providedIn: 'root'
})
export class FirebaseService {
  // Source: https://github.com/AngularTemplates/angular-firebase-crud/blob/master/src/app/services/firebase.service.ts
  constructor(public db: AngularFirestore, private afAuth: AngularFireAuth) { }

  initPoll(pollKey, isDrawn, drawPollCallback) : any {
    return new Promise((resolve, reject) => {
      let votedChoice;
      let choices = [];
      this.getChoices(pollKey).pipe(first()).subscribe(fetchedChoices => {
      fetchedChoices.forEach(choice => {
        const choiceData:any = choice.payload.doc.data();
        const choiceKey:any = choice.payload.doc.id;
        this.getVotes(choiceKey).pipe(first()).subscribe((votes: any) => {
          choices.push({
            id: choiceKey,
            text: choiceData.text,
            votes: votes.length,
            players: choiceData.players
          });
          let currentUserId = this.afAuth.auth.currentUser.uid;
          let hasVoted = votes.filter((vote) => {
            return (vote.payload.doc._document.proto.fields.choice.stringValue == choiceKey) &&
            (vote.payload.doc._document.proto.fields.user.stringValue == currentUserId);
          });
          if (hasVoted.length > 0) {
            votedChoice = hasVoted[0].payload.doc._document.proto.fields.choice.stringValue;
          }
        });
        this.getVotes(choiceKey).subscribe((votes: any) => {
          if (isDrawn) {
            const selectedChoice = choices.find((choice) => {
              return choice.id == choiceKey
            });
            selectedChoice.votes = votes.length;
            drawPollCallback();
          }
        });
      });
      console.log("Done iterating");
    });
    resolve(choices, votedChoice)
    });
  }

}

虽然我没有足够的源代码来确认这些函数的具体行为,但以下代码中的
pipe
subscribe
可能将
forEach
推到异步执行:

this.getChoices(pollKey).pipe(first()).subscribe(fetchedChoices => {
      fetchedChoices.forEach(choice => {...
fetchedChoices=>{fetchedChoices.forEach(…
正在为subscribe函数定义回调函数,该回调函数将在Promise executor函数的执行之外发生。
resolve(choices,votedChoice)
将在调用
subscribe
之后和传递给
subscribe
的回调之前立即执行。
forEach
代码在回调函数中,用于订阅,并将异步调用(以及在承诺解析之后)


并非所有回调都是异步执行的,但如果将其中一个回调传递给名为
subscribe
的函数,则肯定是异步执行的。

看起来您不完全了解代码的哪些部分是异步的,以及代码的哪些部分是按什么顺序执行的

编辑:我假设您代码中的所有可观察对象都是异步的,即它们执行某种API调用以获取所需数据。它们可能是同步的,但您的代码确实不应该这样认为。如果产品生命周期后期的同步调用变为异步,这将大大降低中断某些内容的风险onous.结束编辑

所以,您要问的直接问题是,您需要在订阅之外解决承诺问题-因此,在进入
forEach
循环之前。因此,时间线如下所示:

  • PollComponent
    调用
    firebaseService.initPoll()
  • Promise
    被创建并返回到
    PollComponent
  • PollComponent
    签署承诺
  • 承诺中的Lambda开始执行
  • 你调用
    getChoices()
    observable,创建一些管道并订阅它,我相信这就是你困惑的开始:
    subscribe()
    不会立即触发任何结果,也不会等待可观察管道和订阅lambda中本应执行的任何内容的执行。
    因此,您已经订阅了管道,并立即继续执行剩余的promise lambda的代码
  • 现在,
    Promise
    得到了解决。Observable甚至还没有开始做任何事情,但您已经解决了Promise,它会立即触发订阅链。这是当您的
    then()
    lambda执行时,然后一切都会冷却一段时间
  • 然后在稍后的某个时间,
    observatable
    会发出一个事件,该事件进入您的订阅并触发每个
    forEach
    周期,但是,要从observatable发出您想要的任何内容都为时已晚,因为
    Promise
    已经解决
但另一方面,这似乎只是代码中不同步的几件事情之一管道两次,第一个订阅将某些内容推送到第二个订阅使用的
选项
集合中,这也是完全不同步的,因为在调用
subscribe()时它们不会立即执行
。所以,您需要以这样的方式链接调用,以便后面的步骤只能发生在前面的步骤之后

现在,记住我自己在这个位置上,第一个想法通常是这样的:“好吧,那么,我只需要重新安排我的订阅,并将后一步订阅放在前一步订阅中。”这是显而易见的,也是错误的Rx的整体思想是,您应该只订阅整个管道的最终结果,这通常发生在创建所述管道的服务之外。因此,重新安排代码的正确方法是使用
pipe()
switchMap()
flatMap()构建这样一个管道
combinelatetest()
merge()
map()
等。Rx操作符,这样整个过程最终会产生一个你真正需要的结果,只需通过这个管道,分步进行,而无需对你在那里使用的任何
可观察的
对象显式调用
subscribe()

此外,您不必手动创建
Promise
,实际上,对于这个任务,在可观察对象上有一个简单的操作符

我不知道这段代码在您的情况下是否正确,但以下是如何使用所描述的方法重新安排您的内容的想法。我只希望它足够清楚,以演示如何在您的情况下使用不同的管道操作符替换订阅

initPoll(pollKey, isDrawn, drawPollCallback) : any {

    return this.getChoices(pollKey).pipe(

        first(),

        // flatMap() replaces input value of the lambda
        // with the value that is emitted from the observable returned by the lambda.
        // so, we replace fetchedChoices array with the bunch of this.getVotes(choiceKey) observables
        flatMap((fetchedChoices: any[]) => {

            // here fetchedChoices.map() is a synchronous operator of the array
            // so we get an array of observables out of it and merge them into one observable
            // emitting all the values from all the observables in the array.
            return merge(fetchedChoices.map(choice => {
                const choiceKey: any = choice.payload.doc.id;
                return this.getVotes(choiceKey).pipe(first());
            })).pipe(toArray());
            // toArray() accumulates all the values emitted by the observable it is aplied to into a single array,
            // and emits that array once all observables are completed.

        }),

        // here I feel like you'll need to repeat similar operation
        // but by this time I feel like I'm already lost in your code. :)
        // So I can't really suggest what'd be next according to your code.
        flatMap((choices: any[]) => {
            return merge(choices.map(choice => {
                // ... other processing with calling some services to fetch different pieces of data
            })).pipe(toArray());
        }),

    // and converting it to the promise
    // actually I think you need to consider if you even need it at all
    // maybe observable will do just fine?
    ).toPromise();
}

forEach
不能很好地处理async。感谢您的详细响应。它帮助我更好地理解正在发生的事情。我一步一步地接近它,一次只做一个可观察的操作,但是在第一次使用.pipe(toArray())时,我得到了以下错误核心。js:15723错误:Uncaught(承诺中):TypeError:对象(…)(