簡體   English   中英

Angular 7-Promise解析后,promise中的forEach迭代正在執行。 為什么?

[英]Angular 7 - forEach iteration inside of a promise is executing after the promise resolves. Why?

我創建了一個服務,用於在調用drawPoll()函數之前需要進行的某些操作。 我添加了控制台日志來跟蹤執行順序,但無法弄清楚為什么鏈接到.then()的函數為什么在promise內部的forEach迭代完成之前執行。 創建服務並在Promise中包裝forEach操作的全部目的是使我可以完全確定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;
      }
    }
  }

}

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調用以獲取所需的數據。 它們可能是同步的,但您的代碼實際上不應該假定那樣。 如果產品生命周期后期的同步調用變為異步,這將大大降低破壞某些內容的風險。 結束編輯

因此,您要解決的直接問題是您在預訂之外解決了forEach因此,在進入forEach循環之前。 因此,時間軸是這樣的:

  • PollComponent調用PollComponent firebaseService.initPoll() ;
  • 創建Promise並將其返回給PollComponent
  • PollComponent訂閱諾言;
  • 承諾中的Lambda開始執行;
  • 您要求可觀察到的getChoices() ,創建一些管道並對其進行訂閱,我相信這是您開始困惑的地方: subscribe()不會立即觸發任何結果,並且它不等待執行應在其中執行的任何操作。可觀察的管道和訂閱lambda。 因此,您已經訂閱了管道,並立即繼續執行其他promise lambda的代碼。
  • 現在, Promise得到解決。 Observable甚至還沒有開始做任何事情,但是您已經解決了promise,它立即觸發了then()訂閱鏈。 這是您的then() lambda執行時,然后所有內容冷卻一段時間的時間。
  • 然后,在稍后的某個時間, Observable會發出一個事件,該事件進入您的訂閱並觸發forEach周期, 但是現在發出您想要從該 Observable中獲取的任何內容為時已晚,因為Promise已經解決。

但另一方面,這似乎只是代碼中不同步發生的幾件事之一。 例如,里面的foreach您訂閱this.getVotes(choiceKey)管道的兩倍,和第一簽約的東西推到choices它是由第二認購消耗收集-又一次這是完全同步的,因為他們沒有立即執行時,您調用subscribe() 因此,您需要以這種方式鏈接呼叫,以便以后的步驟只能在前面的步驟之后進行。

現在,回想起自己的位置,第一個想法通常是這樣的:“好吧,我只需要重新排列我的訂閱並將稍后的訂閱放入先前的訂閱中”。 這很明顯是錯誤的。 :) Rx的整體思想是,您應訂閱整個管道的最終結果,該結果通常發生在創建該管道的服務之外。 因此,重新排列代碼的正確方法是使用pipe()switchMap()flatMap()combineLatest()merge()map()等來構建這樣的管道。最終,通過在嬰兒步驟中遍歷此管道,而無需在您使用的任何單個Observable上顯式調用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();
}

盡管我沒有足夠的源代碼來確認這些功能的特定行為,但可能通過pipe傳遞(當然肯定會subscribe )以下代碼將forEach推入異步執行:

this.getChoices(pollKey).pipe(first()).subscribe(fetchedChoices => {
      fetchedChoices.forEach(choice => {...

fetchedChoices => {fetchedChoices.forEach(...正在定義訂閱函數的回調函數,該回調函數將在Promise執行程序函數的執行之外發生resolve(choices, votedChoice)將在調用subscribe和之后立即執行傳遞給回調subscribeforEach代碼是在回調函數訂閱,並且將異步調用(和解決的承諾后)。

並非所有的回調都是異步執行的,但如果將一個回調傳遞給名為subscribe的函數,則可以肯定。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM