Угловой 7 - итерация forEach внутри обещания выполняется после разрешения обещания. Зачем? - PullRequest
0 голосов
/ 12 апреля 2019

Я создал сервис для некоторых манипуляций, которые должны произойти до вызова функции drawPoll ().Я добавил консольные журналы, чтобы отслеживать порядок выполнения и не могу понять, почему функция, связанная с .then (), выполняет ДО того, как итерация forEach внутри обещания завершится.Весь смысл создания службы и включения манипуляции 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)
    });
  }

}

Ответы [ 2 ]

1 голос
/ 12 апреля 2019

Похоже, вы не до конца понимаете, какие части вашего кода асинхронны и в каком порядке выполняются части вашего кода.

РЕДАКТИРОВАТЬ: Я предполагаю, что все наблюдаемые в вашем коде являются асинхронными, то есть они выполняют какие-то вызовы API для получения необходимых данных. Они могут быть синхронными, но ваш код на самом деле не должен предполагать это. Это значительно снизит риск поломки, если синхронный вызов на более позднем этапе жизни продукта станет асинхронным. КОНЕЦ РЕДАКТИРОВАНИЯ

Итак, немедленная проблема, о которой вы спрашиваете, заключается в том, что вы разрешаете обещание вне подписки - следовательно, прежде чем перейти в цикл forEach. Итак, график таков:

  • PollComponent звонки firebaseService.initPoll();
  • Promise создается и возвращается в PollComponent;
  • PollComponent подписывается на обещание;
  • Лямбда в обещании начинает исполняться;
  • Вы вызываете getChoices() наблюдаемый, создаете какой-то конвейер и подписываетесь на него, и я считаю, что именно здесь начинается ваше замешательство: subscribe() не вызывает никаких результатов немедленно и не ожидает выполнения все, что должно было быть выполнено в наблюдаемой конвейерной и подписной лямбде. Итак, вы подписались на конвейер и сразу же приступили к выполнению кода остальной части лямбда обещания.
  • Теперь Promise разрешается. Observable даже не начал ничего делать, но вы уже выполнили обещание, которое немедленно вызывает цепочку then() подписок. Это когда ваша then() лямбда запускается, а затем все остывает на некоторое время.
  • Затем через некоторое время Observable испускает событие, которое попадает в вашу подписку и запускает цикл forEach, , но уже слишком поздно излучать то, что вы хотели получить из наблюдаемого , потому что Promise уже решена.

Но, с другой стороны, похоже, что это только одна из нескольких вещей, которые не синхронизированы в вашем коде. Например, внутри foreach вы подписываетесь на this.getVotes(choiceKey) каналов дважды, и первая подписка помещает что-то в коллекцию choices, которая используется второй подпиской - и опять-таки это полностью не синхронизировано, потому что они не выполняются сразу, когда вы Звоните subscribe(). Таким образом, вам нужно объединить вызовы таким образом, чтобы последующий шаг мог выполняться только после более раннего.

Теперь, вспоминая себя в этой самой позиции, первая мысль обычно такова: «Хорошо, тогда мне просто нужно изменить порядок моих подписок и поместить подписку на более поздний шаг в подписку с более ранним шагом». Это так же очевидно, как и неправильно. :) Вся идея Rx заключается в том, что вы должны только подписаться на конечный результат всего конвейера, который обычно происходит за пределами службы, которая создает указанный конвейер. Таким образом, правильный способ реорганизации вашего кода состоит в том, чтобы построить такой конвейер с операторами pipe(), switchMap(), flatMap(), combineLatest(), merge(), map() и т. Д., Чтобы все это в итоге получим один единственный результат, который вам действительно нужен, перемещаясь по этому конвейеру шаг за шагом, без явного вызова 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();
}
0 голосов
/ 12 апреля 2019

Хотя мне не хватает исходного кода для подтверждения конкретного поведения этих функций, возможно, pipe и, конечно, subscribe в следующем коде толкают forEach в асинхронное выполнение:

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

fetchedChoices => {fetchedChoices.forEach(... определяет функцию обратного вызова для функции подписки, которая произойдет вне выполнения функции исполнителя Promise.resolve(choices, votedChoice) будет выполняться сразу после вызова на subscribe и до того, как обратный вызов будет передан на subscribe.Код forEach находится в функции обратного вызова для подписки и будет вызываться асинхронно (и после разрешения обещания).

Not все обратные вызовы выполняются асинхронно, но это хорошоДержу пари, что если передать его в функцию с именем subscribe, это будет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...