如何使RxJS Observable在特定日期时间发射?

时间:2018-10-31 11:25:46

标签: javascript rxjs

我有一个RxJS Observable,需要在特定时间重新计算,如DateTime对象数组所描述(尽管出于这个问题的目的,它们可能是JavaScript Date对象,纪元毫秒或代表特定时间点的其他任何东西):

const changeTimes = [
    //            yyyy, mm, dd, hh, mm
    DateTime.utc( 2018, 10, 31, 21, 45 ),
    DateTime.utc( 2018, 10, 31, 21, 50 ),
    DateTime.utc( 2018, 10, 31, 22, 00 ),
    DateTime.utc( 2018, 10, 31, 23, 00 ),
    DateTime.utc( 2018, 10, 31, 23, 30 ),
];

我正在努力了解如何创建一个在这样的数组中指定的时间发射的Observable。

这就是我想回答我自己的问题的想法:

  • 几乎可以肯定我需要使用the delay operator,其中指定的延迟是“现在”到下一个未来日期时间之间的时间。
  • 我某种程度上需要确保“现在”在订阅时是当前的,而不是在创建可观察的时(可能通过使用the defer operator),尽管我不想不必要地创建多个Observable实例,如果有多个订阅。
  • 我不确定随着时间的流逝如何遍历数组— the expand operator可能是我所需要的,但是它递归地调用了某些东西,而我只是想遍历列表。
  • The timer operator似乎无关紧要,因为每个日期时间之间的持续时间是不同的。
  • 我可以将每个日期时间映射到它自己的延迟Observable并通过merge将它们全部返回,但是随着数组中日期时间数量的增加(可能有数百个),这变得非常低效。不得已。

如何使RxJS Observable接收日期时间列表,然后在到达每个时间时发出,并在最后一个时间完成?

4 个答案:

答案 0 :(得分:1)

我认为您在要点中概述的内容都是正确的。使用delay似乎很明显,但会使链条难以理解。

我想到的解决方案是假设您在创建可观察链之前就知道changeTimes数组。您可以创建自己的“可观察的创建方法”,该方法将返回一个基于setTimeout发出的Observable(例如,这只是“伪代码”,它无法正确计算日期):

const schedule = (dates: Date[]): Observable<Date> => new Observable(observer => {
  // Sort the `dates` array from the earliest to the latest...

  let index = 0;
  let clearTimeout;

  const loop = () => {
    const now = new Date();
    const delay = dates[index] - now;

    clearTimeout = setTimeout(() => {
      observer.next(dates[index++]);

      if (index < dates.length) {
        loop();
      }
    }, delay);
  }

  loop();

  return () => clearTimeout(clearTimeout);
}); 

...

schedule(changeTimes)
  .subscribe(...)

您在merge上提到的最后一个选项实际上还不错。我了解您担心它会创建很多订阅,但是如果您对changeTimes数组进行排序,然后使用concat而不是merge,它将始终仅保留一个活动订阅即使您创建了100个Observable。

答案 1 :(得分:0)

这是一个可行的示例:

import {Injectable} from '@angular/core';
import {Observable, Subject, timer} from 'rxjs';

@Injectable()
export class TimerService {

  futureDates: Date[] = [];
  futureDate: Date;
  notifier: Observable<string>;

  cycle = (observer) => {
    if (this.futureDates.length > 0) {
      this.futureDate = this.futureDates.shift();

      const msInFuture = this.futureDate.getTime() - Date.now();
      if (msInFuture < 0) {
        console.log(`date ${this.futureDate.toISOString()}
            expected to be in the future, but was ${msInFuture} msec in the past, so stopping`);

        observer.complete();
      } else {
        timer(msInFuture).subscribe(x => {
          observer.next(`triggered at ${new Date().toISOString()}`);
          this.cycle(observer);
        });
      }
    } else {
        observer. complete();
    }
  }

  getTimer(): Observable<string> {
    const now = new Date();
    const ms1 = now.getTime() + 10000;
    const ms2 = now.getTime() + 20000;
    this.futureDates.push(new Date(ms1));
    this.futureDates.push(new Date(ms2));

    this.notifier = new Observable(observer => {
      this.cycle(observer);
    });

    return this.notifier;
  }
}

在此示例中,未来时间列表是在getTimer()方法中创建的,但是您可以将一系列日期传递给该方法。

关键是简单地存储日期,一次处理一个,然后在处理时间,检查该日期在将来的时间,并为该毫秒数设置一个一次性Rx计时器。

答案 2 :(得分:0)

给出DateTime个对象的数组:

const changeTimes = [
    //            yyyy, mm, dd, hh, mm
    DateTime.utc( 2018, 10, 31, 21, 45 ),
    DateTime.utc( 2018, 10, 31, 21, 50 ),
    DateTime.utc( 2018, 10, 31, 22, 00 ),
    DateTime.utc( 2018, 10, 31, 23, 00 ),
    DateTime.utc( 2018, 10, 31, 23, 30 ),
];

或者更好的是,一个Observable每次数组更改时都会发出一个数组(这是我的情况下实际发生的情况,尽管我没有在问题中提及它,因为它并不严格相关):

const changeTimes$: Observable<DateTime[]> = /* ... */;

以下Observable将立即发出订阅的下一个将来时间,在上一个将来时间过去时发出每个以后的将来时间,然后完成null

const nextTime$ = changeTimes$.pipe(
    // sort DateTimes chronologically
    map(unsorted => [...unsorted].sort((x, y) => +x - +y),
    // remove duplicates
    map(duplicated => duplicated.filter((item, i) => !i || +item !== +duplicated[i - 1])),
    // convert each time to a delayed Observable
    map(times => [...times, null].map((time, i) => defer(() => of(time).pipe(
        // emit the first one immediately
        // emit all others at the previously emitted time
        delay(i === 0 ? 0 : +times[i - 1] - +DateTime.utc())
    )))),
    // combine into a single Observable
    switchMap(observables => concat(...observables)),
);
  • 排序是必要的,因为每个内部Observable(“等待X毫秒然后报告时间”)都在前一个完成时订阅。
  • 严格来说,删除重复项不是必需的,但似乎很适合。
  • defer用于在订阅内部Observable时计算当前时间。
  • concat用于连续执行每个内部Observable(由于martin),从而避免了同时在列表中每次预订的开销。

这满足了我最初的需求:

  

我有一个RxJS Observable,它需要在特定的时间重新计算,如DateTime对象数组所描述的那样。

是如果我将其与需要使用the combineLatest operator进行重新计算的数据结合以在正确的时间触发该重新计算:

const timeAwareData$ = combineLatest(timeUnawareData$, nextTime$).pipe(
    tap(() => console.log('either the data has changed or a time has been reached')),
    // ...
);

我不满意我的解决方案

它同时为列表中的每个时间创建一个单独的内部Observable。我觉得有可能重构,以便每个内部Observable仅在销毁前一个内部Observable之后创建。任何改进技巧将不胜感激。

答案 3 :(得分:-1)