javascript - Angular 7 - 承诺内的 forEach 迭代在承诺解决后执行。为什么?
问题描述
我为调用 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 调用以获取所需的数据。它们可能是同步的,但您的代码确实不应该这样假设。如果产品生命周期后期的同步调用变为异步,这将大大降低破坏某些东西的风险。结束编辑
因此,您要问的直接问题是您在订阅之外解决承诺 - 因此在您进入forEach
循环之前。所以,时间线是这样的:
PollComponent
来电firebaseService.initPoll()
;Promise
被创建并返回到PollComponent
;PollComponent
同意承诺;- Promise 中的 Lambda 开始执行;
- 您调用
getChoices()
observable,创建一些管道并订阅它,我相信这就是您的困惑开始的地方:subscribe()
不会立即触发任何结果,并且它不会等待执行任何应该在 observable 管道和订阅 lambda 中执行的操作. 因此,您已经订阅了管道并立即继续执行其余 promise lambda 的代码。 - 现在,
Promise
得到解决。Observable 甚至还没有开始做任何事情,但是你已经解决了 Promise,它立即触发then()
了订阅链。这是你的then()
lambda 执行的时候,然后一切都会冷却一段时间。 - 然后在稍后的某个时间
Observable
发出一个进入您的订阅并触发forEach
循环的事件,但是现在发出您想从 observable 获得的任何内容都为时已晚,因为Promise
已经解决了。
但另一方面,这似乎只是代码中不同步发生的几件事之一。例如,在 foreach 中,您订阅了this.getVotes(choiceKey)
两次管道,第一次订阅将某些内容推送到choices
第二次订阅使用的集合中——这又完全不同步,因为当您调用subscribe()
. 因此,您需要以这样的方式链接调用,以便后面的步骤只能发生在前面的步骤之后。
现在,想起自己处于这个位置,第一个想法通常是这样的:“好吧,我只需要重新排列我的订阅并将后一步订阅放在前一步订阅中”。这很明显,因为它是错误的。:) Rx 的整个想法是,您应该只订阅整个管道的最终结果,这通常发生在创建所述管道的服务之外。因此,重新排列代码的正确方法是使用pipe()
, switchMap()
, flatMap()
, combineLatest()
,等 Rx 运算符构建这样一个管道merge()
,map()
这样整个事情就会产生一个你真正需要的结果,通过这个管道在婴儿中移动无需显式调用您在那里使用subscribe()
的任何单个的步骤Observable
。
此外,您不必Promise
手动创建,实际上在 observable 上有一个简单的运算符可用于此任务。
我不知道这是否是您的情况下的正确代码,但以下是您如何使用所描述的方法重新排列您的东西的想法。我只希望在您的情况下演示如何用不同的管道运营商替换订阅足够清楚。
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 - 在 React 上下文状态中更新单个项目
- javascript - 如何在文本输入模式上设置焦点
- python - Asyncio done() 检查异常
- r - R迭代数据集生成
- android-studio - Android Studio 布局设计视图不显示菜单栏项?
- ios - 反应导航 5 禁用向后滑动动作
- azure - 如何成为 Azure 门户的全局管理员?
- javascript - 选择一种在 ASP.NET 中转义 JavaScript 的方法
- r - 根据行 ID 将“文件名”列分配给数据框
- javascript - 如何在 React 中根据一天中的时间调用函数并更改标题文本和颜色?