javascript - Angular 7 - promise 内的 forEach 迭代在 promise 解决后执行。为什么?

标签 javascript angular typescript promise

我为调用 drawPoll() 函数之前需要进行的一些操作创建了一个服务。我添加了控制台日志来跟踪执行顺序,并且无法弄清楚为什么链接到 .then() 的函数在 promise 内的 forEach 迭代完成之前执行。创建服务并将 forEach 操作包装在 Promise 中的全部意义在于,我可以绝对确定在调用 drawPoll() 函数之前 forEach 迭代已经完成。我在这里想念什么?

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;
      }
    }
  }

}

firebase.service.ts
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)
    });
  }

}

最佳答案

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

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

因此,您要问的直接问题是您在订阅之外解决 promise - 因此在您进入 forEach 之前环形。所以,时间线是这样的:

  • PollComponent来电firebaseService.initPoll() ;
  • Promise被创建并返回到 PollComponent ;
  • PollComponent同意 promise ;
  • Promise 中的 Lambda 开始执行;
  • 您调用getChoices()可观察的,创建一些管道并订阅它,我相信这是你开始困惑的地方: subscribe()不会立即触发任何结果,也不会等待执行任何应该在可观察管道和订阅 lambda 中执行的操作。 因此,您已经订阅了管道并立即继续执行其余 promise lambda 的代码。
  • 现在,Promise得到解决。 Observable 甚至还没有开始做任何事情,但你已经解决了 Promise,它立即触发 then()订阅链。这是您的then() lambda 执行,然后一切都冷却了一段时间。
  • 然后在稍后的某个时间Observable发出一个进入您的订阅并触发 forEach 的事件循环,但是现在发出你想从可观察到的 中得到的任何东西都为时已晚因为Promise已经解决了。

  • 但另一方面,这似乎只是代码中不同步发生的几件事之一。例如,在 foreach 中订阅 this.getVotes(choiceKey)管道两次,第一次订阅将某些内容推送到 choices第二个订阅使用的集合 - 这完全不同步,因为当您调用 subscribe() 时它们不会立即执行.因此,您需要以这样的方式链接调用,以便后面的步骤只能发生在前面的步骤之后。

    现在,想起自己处于这个位置,第一个想法通常是这样的:“好吧,我只需要重新排列我的订阅并将后一步订阅放在前一步订阅中”。这很明显,因为它是错误的。 :) Rx 的整个想法是你应该 仅限 订阅整个管道的最终结果,这通常发生在创建所述管道的服务之外。因此,重新排列代码的正确方法是使用 pipe() 构建这样的管道。 , switchMap() , flatMap() , combineLatest() , merge() , map()等等。Rx 运算符,这样整个事情就会产生一个你最终真正需要的结果,方法是在小步骤中通过这个管道而不显式调用 subscribe()在任何一个 Observable你在那里使用。

    此外,您不必创建 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();
    }
    

    关于javascript - Angular 7 - promise 内的 forEach 迭代在 promise 解决后执行。为什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55642724/

    相关文章:

    javascript - 两个旋转元素之间的碰撞

    javascript - css-loader URL root 不修改 url

    angular - AADSTS70002 : The request body must contain the following parameter: 'client_secret or client_assertion'

    javascript - HowlerJS - 音频播放器 - 错误 401(未经授权)

    Angular 8下拉选择的选项没有显示,这是怎么回事?

    typescript - 如何检查 typescript 中的未定义

    typescript - 混淆 ES6 模块语法和 Typescript 模块语法

    javascript - Vue-GTM错误: You may need an appropriate loader to handle this file type

    javascript - 如何在 GitHub 和 npm 上用 Javascript/Typescript 创建我的第一个库

    javascript - 仅在单击按钮时打开垫扩展面板