[英]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
訂閱諾言; 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
和之后立即執行傳遞給回調subscribe
該forEach
代碼是在回調函數訂閱,並且將異步調用(和解決的承諾后)。
並非所有的回調都是異步執行的,但如果將一個回調傳遞給名為subscribe
的函數,則可以肯定。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.