【问题标题】:using pipe async always stucks on loading使用管道异步总是卡在加载
【发布时间】:2020-08-31 21:25:22
【问题描述】:

我正在使用 switchMapcombineLatest 将多个可观察对象加入到一个全局 joined$ 可观察对象中

这是组件 TS 文件

export class ProgressComponent implements OnInit{

    user: User;
    joined$: Observable<any>;

    constructor(
        protected tasksService: TasksService,
        protected courseService: CoursesService,
        protected fieldService: FieldsService,
        protected sessionService: SessionsService,
        protected applicationService: ApplicationForSessionsService,
        protected TaskdatesService: SessionTaskDatesService,
        protected router: Router,
        private userStore: UserStore)
    {}

    ngOnInit(): void {
        this.user = this.userStore.getUser();
        this.joined$ = this.applicationService.list(1, 10, undefined, this.user.id)
            .pipe(
                switchMap(applications => {
                    const courseSessionIds = uniq(applications.map(application => application.courseSessionId))

                    return combineLatest(
                        of(applications),
                        combineLatest(
                            courseSessionIds.map(courseSessionId => this.sessionService.get(courseSessionId).pipe(
                                switchMap(session => {
                                    const courseId = session.courseId
                                    return combineLatest(
                                        of(session),
                                        combineLatest(
                                            this.courseService.get(courseId).pipe(
                                                switchMap(course => {
                                                    const fieldId = course.fieldId

                                                    return combineLatest(
                                                        of(course),
                                                        combineLatest(
                                                            this.fieldService.get(fieldId).pipe(
                                                                map (field => field)
                                                            )
                                                        )
                                                    )
                                                }),
                                                map(([course, field]) => {
                                                    return {...course, field: field.find(f => f.id == course.fieldId)}
                                                })
                                            )
                                        ),
                                                                         
                                    )
                                }),
                                map(([session, course]) =>  {
                                    return {
                                        ...session, 
                                        course: course.find(c => c.id === session.courseId)
                                    }
                                }),
                                switchMap( session => {
                                    const sessionId = session.id;

                                    return combineLatest(
                                        of(session),
                                        combineLatest(
                                            this.TaskdatesService.getBySessionId(sessionId).pipe(
                                                switchMap(dates => {
                                                    const taskDatesIds = uniq(dates.map(dt => dt.taskId));

                                                    return combineLatest(
                                                        of(dates),
                                                        combineLatest(
                                                            taskDatesIds.map(taskDateId => this.tasksService.get(taskDateId).pipe(
                                                                map(task => task)
                                                            ))
                                                        )
                                                    )
                                                }),
                                                map(([dates, task]) => {
                                                    return dates.map(date => {
                                                        return {...date, task: task.find(t => t.id === date.taskId)}
                                                    })
                                                
                                                })
                                            )
                                        )
                                    )
                                }),
                                map(([session, dates]) => {
                                    return {
                                        ...session,
                                        dates: dates.map(date => date.find(d => d.sessionId === session.id))
                                    }
                                })
                            ))
                        )
                    )
                }),
                map(([applications, session]) => {
                    return applications.map(app => {
                        return {
                            ...app,
                            session: session.find(s => s.id === app.courseSessionId)
                        }
                    })
                })
            );
    }
}

这里是 HTML 模板文件

    <ng-container *ngIf="joined$ | async; else loading; let joined">

    <div *ngFor="let application of joined">
        <div class="current-application">
            <nb-card>
                <nb-card-header>
                    <h4>{{application.session.course.name}}</h4>
                    <small><i>{{application.session.course.field.name}}</i></small>
                </nb-card-header>
                <nb-card-body>
                    <p><b>id: </b>{{application.id}}</p>
                    <p><b>applicationDate: </b>{{application.applicationDate}}</p>
                    <p><b>acceptedDate: </b>{{application.acceptedDate}}</p>
                    <hr>
                    <p><b>session Id: </b>{{application.session.id}}</p>
                    <p><b>session capacity: </b>{{application.session.capacity}}</p>
                    <p><b>session startDate: </b>{{application.session.startDate}}</p>
                    <p><b>session endDate: </b>{{application.session.endDate}}</p>
                    <hr>
                    <p><b>Course Id: </b>{{application.session.course.id}}</p>
                    <p><b>Course Id: </b>{{application.session.course.id}}</p>
                </nb-card-body>
            </nb-card>
        </div>
    </div>

</ng-container>

<ng-template #loading>
    <p>Loding ...</p>
</ng-template>

编辑:调试后发现日期数组为空时会出现错误,所以解决方法是对日期数组的长度进行测试。问题是当我尝试创建条件时出现以下错误

'(dates: SessionTaskDate[]) => void' 类型的参数不能分配给'(value: SessionTaskDate[], index: number) => ObservableInput' 类型的参数。 类型“void”不可分配给类型“ObservableInput”。

在以下情况下触发:

switchMap(dates =>{
        if(dates.length > 0){
            dates.map(date => this.augmentDateWithTask(date))
        }
    })

【问题讨论】:

  • 这代码太多了。乍一看:我认为你不应该在 combinelatest 中使用 switchmap(在 combineLates 之后使用它,并且当你的会话值已经可用时,你不必执行 of(session) ,仅举几个问题
  • 这段代码非常复杂,难以理解。我建议把它分成小块。如果您的模板总是卡在加载中,那是因为joined$ 从未发出值。当使用combineLatest 时,它不会发出任何值,直到它的所有源 observables 都发出,所以这可能是你的问题。
  • 您不需要使用ofcombineLatest。它们都是生成 observable 的函数。 combineLatest 在你有 2 个或更多源 observables 时使用。在您的情况下,您似乎只有一个来源,applicationService.list()。所以看起来你根本不需要combineLatest
  • 我关注了这个tutorial我只需要内部加入这些实体。

标签: angular typescript rxjs switchmap combinelatest


【解决方案1】:

如果我理解正确,您有一系列应用程序,这些应用程序由this.applicationService.list 返回的 Observable 通知。

然后每个应用程序都有一个courseSessionId,您可以使用它通过this.sessionService.get方法获取课程会话详细信息。

然后每个会话都有一个courseId,您可以使用它通过this.courseService.get获取课程详细信息。

然后每个课程都有一个fieldId,您可以使用它通过this.fieldService.get获取field详细信息。

现在您应该有一个 sessions 数组,其中还包含他们所指的课程的所有详细信息。

然后,对于每个会话,您需要通过this.TaskdatesService.getBySessionId 获取日期

Dates 似乎是包含 taskDatesId 的对象。您收集所有 taskDatesIds 并通过 this.tasksService.get 获取 task 详细信息。

现在您已经拥有了所有 dates 和所有 tasks,对于每个 date,您使用 date创建一个新对象> 属性及其相关的任务

然后您返回到 session,您将创建一个具有所有 session 属性及其相关 日期 的新对象。

现在您有了一个对象,其中包含所有 会话 详细信息、它所引用的 课程 的所有详细信息以及它所指的 日期 的所有详细信息有。

最后一步是为每个应用程序创建一个新对象,其中包含应用程序的所有属性以及“增强”会话的所有属性/em> 刚刚创建的对象。

相当有逻辑。

所以,再一次,如果这是正确的理解,我会从最内在的请求向外处理问题。

第一个内部请求是从 course 开始返回一个 Observable 的请求,该 Observable 发出一个具有所有 course 属性和 field的对象> 细节(我们称这个对象为augmentedCourse)。这可以通过这样的方法来执行

augmentCourseWithField(course) {
  return this.fieldService.get(course.fieldId).pipe(
    map (field => {
      return {...course, field}
    })
  )
}

然后我会向外迈出一步,创建一个方法,该方法从 courseId 开始,返回一个 Observable,该 Observable 发出一个 augmentedCourse,即具有所有课程字段详细信息。

fetchAugmentedCourse(courseId) {
  return this.courseService.get(courseId).pipe(
     switchMap(course => this.augmentCourseWithField(course))
  )
}

现在让我们更进一步,创建一个方法,从 session 开始,返回一个对象,该对象包含 session 的所有属性以及augmentedCourse session 指的是。我们称这个对象为 augmentedSession

augmentSessionWithCourse(session) {
  return this.fetchAugmentedCourse(session.courseId).pipe(
    map(course => {
      return {...session, course}
    })
  )
}

现在又往外迈了一步。我们想要从 courseSessionId 开始获取 augmentedSession

fetchAugmentedSession(courseSessionId) {
  return this.sessionService.get(courseSessionId).pipe(
     switchMap(session => this.augmentSessionWithCourse(session))
  )
}

那么,到目前为止,我们取得了什么成就?我们能够创建一个从 courseSessionId 开始发出 augmentedSession 的 Observable。

虽然我们有一个应用程序列表,但在最外层,每个应用程序都包含一个courseSessionId。不同的应用程序可以共享相同的课程,因此具有相同的courseSessionId。因此,获取所有 应用程序,创建唯一 courseSessionIds 列表,使用该列表获取所有 课程,然后分配给每个应用程序它的课程。这样我们就避免了对同一课程多次查询后端。

可以这样实现

fetchAugmentedApplications(user) {
  return this.applicationService.list(1, 10, undefined, user.id).pipe(
    switchMap(applications => {
      const courseSessionIds = uniq(applications.map(application => application.courseSessionId));
      // create an array of Observables, each one will emit an augmentedSession
      const augSessObs = courseSessionIds.map(sId => fetchAugmentedSession(sId))
      // we can use combineLatest instead of forkJoin, but I prefer forkJoin
      return forkJoin(augSessObs).pipe(
        map(augmentedSessions => {
          return applications.map(app => {
            const appSession = augmentedSessions.find(s => s.id === app.courseSessionId);
            return {...app, session: appSession}
          });
        })
      )
    })
  )
}

使用类似的样式,您应该还可以将日期 详细信息添加到您的会话。同样在这种情况下,我们从最里面的操作开始,即通过his.tasksService.get 检索给定taskIds 数组的tasks 数组。

代码如下所示

fetchTasks(taskIds) {
   taskObs = taskIds.map(tId => this.tasksService.get(tId));
   return forkJoin(taskObs)
}

向外移动一步,我们可以将 dates 数组的每个 date 扩充为属于该 date 像这样

augmentDates(dates) {
  if (dates.length === 0) {
    return of([]);
  }
  const taskDatesIds = uniq(dates.map(dt => dt.taskId));
  return fetchTasks(taskDatesIds).pipe(
    map(tasks => {
      return dates.map(date => {
        return {...date, task: task.find(t => t.id === date.taskId)}
      })
    })
  )
}

现在我们可以像这样用它的日期扩充一个会话

augmentSessionWithDates(session) {
  return this.TaskdatesService.getBySessionId(session.id).pipe(
    switchMap(dates => augmentDates(dates).pipe( 
      map(augmentedDates => {
        return {...session, dates: augmentedDates};
      })
    ))
  )
}

我们现在可以完成fetchAugmentedSession 方法,同时添加像这样的日期信息来扩充会话的部分

fetchAugmentedSession(courseSessionId) {
  return this.sessionService.get(courseSessionId).pipe(
     switchMap(session => this.augmentSessionWithCourse(session)),
     switchMap(session => this.augmentSessionWithDates(session)),
  )
}

通过这种方式,您将逻辑拆分为更小的块,这些块更易于测试,并且希望更易于阅读。

我没有任何操场来测试代码,所以很可能其中有错别字。我希望逻辑足够清晰。

【讨论】:

  • 谢谢,您的方法澄清了很多事情,如果我删除 Dates 数组部分,最终可观察到的增强应用程序的唯一问题是可以正常工作。我发现创建字段和课程更容易,因为它们只是对象中的一个属性。主要问题在于 Dates[] 数组
  • 日期数组为空时触发的问题。我不知道如何对其应用条件,请参阅编辑
  • 重点是你作为参数传递给switchMap的函数必须返回一个Observable。在您的代码中,如果 dates 为空,您将返回 void 因此错误。我尝试编辑我的回复,并为此案例提出建议。
【解决方案2】:

这是很多异步嵌套,也许还有其他方法可以解决这个问题?

如果您尝试获取多个 observables 并拥有一个订阅(例如,能够以类似的方式对多个事件做出反应),那么请使用 rxjs merge。

如果您想做很多单独的订阅,例如订阅 users、foo 和 bar,然后将它们的所有值用于某些逻辑,请使用 rxjs forkJoin。

如果你陈述一个明确的目标,提供指导会更容易,但我知道 rxjs 运算符嵌套的级别不容易维护。

【讨论】:

  • 我只需要从 5 个不同实体中获取数据,这些实体加入特定结构,然后在对其应用次要逻辑后将其呈现在模板中。我写的代码灵感来自这个article
  • 根据你需要的结构,我仍然觉得你应该能够只做一个大的 combineLatest。类似于: combineLatest(timerOne$, timerTwo$, timerThree$).subscribe( ([timerValOne, timerValTwo, timerValThree]) => { 见learnrxjs.io/learn-rxjs/operators/combination/combinelatest
  • 如果我排除了日期数组的一部分,上面的代码工作正常,当我将解决方案应用于另一个问题时,我也面临同样的问题。另外,另一个答案比我解释的逻辑更多。
  • 日期数组为空时触发的问题。我不知道如何对其应用条件,请参阅编辑
猜你喜欢
  • 2023-03-29
  • 2019-08-06
  • 2016-02-18
  • 2018-09-11
  • 2017-02-12
  • 2020-03-02
  • 2017-09-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多