时间操作符

这可不是一个简单的话题。其中涉及了应用程序中的诸多领域,你可能想要同步 API 的响应,或者你想要处理其它类型的流,比如 UI 中的点击事件或键盘事件。

有大量的操作符以它们各自的方式来处理时间,比如 delaydebouncethrottleinterval, 等等。

interval

这个操作符用来创建一个 Observable,基本上它所做的就是按固定的时间间隔提供值,函数签名如下:

  1. Rx.Observable.interval([ms])

示例用法:

  1. Rx.Observable.interval(100)
  2. // 无限生成下去

因为这个操作符会不停地生成值,所以倾向于和 take() 操作符一起使用,这样可以在调用它之前限制生成值的数量,就像这样:

  1. Rx.Observable.interval(1000).take(3)
  2. // 生成 1,2,3

timer

timer 是个有趣的操作符,它可以有多种行为,这取决于你如何使用它。它的函数签名是

  1. Rx.Observable.timer([initial delay],[thereafter])

然后只有第一个参数是必须的,所以取决于使用参数的数量,它会有不同的用法。

一次性的

  1. let stream$ = Rx.Observable.timer(1000);
  2. stream$.subscribe(data => console.log(data));
  3. // 1秒后生成0

这样就是一次性的,因为并没有规定何时发出下一个值。

指定第二个参数

  1. let moreThanOne$ = Rx.Observable.timer(2000, 500).take(3);
  2. moreThanOne$.subscribe(data => console.log('timer with args', data));
  3. // 2秒后生成0,然后再500毫秒后生成1,然后再500毫秒生成2

这样更灵活一些,会根据第二个参数持续性的发出值。

delay

delay() 操作符只是简单地延迟每个要发出的值,它是这样使用的:

  1. var start = new Date();
  2. let stream$ = Rx.Observable.interval(500).take(3);
  3. stream$
  4. .delay(300)
  5. .subscribe((x) => {
  6. console.log('val',x);
  7. console.log( new Date() - start );
  8. })
  9. // 800毫秒左右后输出 0 , 1300毫秒左右后输出1, 1800毫秒左后后输出2

业务场景

delay 操作符可以在很多地方使用,但其中一个很好的场景是错误处理,尤其是当网络不稳定时我们想要在x毫秒后重试整个流:

想了解更多,请阅读错误处理章节

sampleTime

我通常认为这个场景可以称之为“懒得理你”。我的意思是事件只会在特定的时间点被触发。

业务场景

所以在x毫秒内的忽略事件的能力是非常有用的。想象一下,一个保存按钮被狂点N次,只在x毫秒后只有最近的一次点击生效而忽略其它的点击不是很好吗?

  1. const btn = document.getElementById('btnIgnore');
  2. var start = new Date();
  3. const input$ = Rx.Observable
  4. .fromEvent(btn, 'click')
  5. .sampleTime(2000);
  6. input$.subscribe(val => {
  7. console.log(val, new Date() - start);
  8. });

上面的代码所做的就是这件事。

debounceTime

debounceTime() 操作符会告诉你:我只会以一定的时间间隔发出数据,而不会一直发出数据。

业务场景

Debounce 是一个已知的概念,特别是当你敲击键盘的时候。就像是在说,我们不在乎你的每次敲击键盘,但是一旦你停止打字后的一段时间是我们所关心的。一个普通的 auto complete (自动完成/智能提示) 就应该在这个时候开始启动了。如果说你的用户停止打字已经有x毫秒了,通常这意味着我们应该执行一次 ajax 调用并取回结果。

  1. const input = document.getElementById('input');
  2. const example = Rx.Observable
  3. .fromEvent(input, 'keyup')
  4. .map(i => i.currentTarget.value);
  5. // 在两次敲击键盘事件之间,有0.5秒的等待时间,如果时间小于0.5秒则丢弃前一个敲击键盘事件
  6. const debouncedInput = example.debounceTime(500);
  7. const subscribe = debouncedInput.subscribe(val => {
  8. console.log(`Debounced Input: ${val}`);
  9. });

上面的代码只会输出一个值,值来源于 input 表单,在你停止打字后的500毫秒后,才值得它报告一下,也就是发出一个值。

throttleTime

TODO

buffer

buffer 操作符的能力是在输出它的值前记录x个发出的值,它可以使用一个或两个参数。

  1. .buffer( whenToReleaseValuesStartObservable )
  2. .buffer( whenToReleaseValuesStartObservable, whenToReleaseValuesEndObservable )

那么这意味着什么呢?它的意思是,如果我们有一个点击流的话,可以将其我切成漂亮的小块流,每一小块流包含的事件的数量都是相同的。使用一个参数的话,我们可以给它一个时间参数(译者注: 这里说时间参数可能不太准确,会让人联想到500毫秒这样的参数,应该是时间相关的 Observable,比如 interval 操作符生成的),假设是500毫秒。所以原本要发出的值会积攒500毫秒后发出,然后另一个 Observable 会开启,老的 Observable 则被抛弃。这很像是在使用秒表,一次记录500毫秒。示例:

  1. let scissor$ = Rx.Observable.interval(500)
  2. let emitter$ = Rx.Observable.interval(100).take(10) // 总共会输出10个值
  3. .buffer( scissor$ )
  4. // 500毫秒后输出: [0,1,2,3,4] 1秒后输出: [5,6,7,8,9]

弹珠图

  1. --- c --- c - c --- >
  2. -------| ------- |- >
  3. 结果流是 :
  4. ------ r ------- r r -- >

业务场景

那么 buffer 的业务场景到底是什么? 那就是双击,对单击作出响应显然是很简单的,但如果只想对双击或是三连击进行处理,又应该如何用代码来处理呢?你可能会用类似下面的方法来处理:

  1. $('#btn').bind('click', function(){
  2. if(!start) { start = timer.start(); }
  3. timePassedSinceLastClickInMs = now - start;
  4. if(timePassedSinceLastClickInMs < 250) {
  5. console.log('double click');
  6. } else {
  7. console.log('single click')
  8. }
  9. start = timer.start();
  10. })

看下上面的伪代码。关键是你需要记录一些在点击之间的时间变量。这样的代码缺乏优雅性,不能算是一种好的代码

在 RxJS 中建模

到目前为止,对于 RxJS 我们所知道的一切都是关于流和随着时间的推移对值进行建模。点击没什么不同,它们也是随着时间的推移而产生。

  1. ---- c ---- c ----- c ----- >

然而,我们关心的是短时间内接连出现的点击,即像下面这样的双击或三连击:

  1. --- c - c ------ c -- c -- c ----- c

从上面的流中你应该可以推断出发生了一次双击、一次三连击和一次单击。

假设我就是这么做的,那么然后呢?你希望流自己分组,以便告诉我们这一点,即需要将哪些点击作为一个组发出。filter() 操作符可以帮我们完成任务。如果我们定义了一个足够长的时间(假设是300毫秒)来收集事件,可以使用下面的代码将时间从0到永远以300毫秒的时间块进行分割:

  1. let clicks$ = Rx.Observable.fromEvent(document.getElementById('btn'), 'click');
  2. let scissor$ = Rx.Observable.interval(300);
  3. clicks$.buffer( scissor$ )
  4. //.filter( (clicks) => clicks.length >=2 )
  5. .subscribe((value) => {
  6. if(value.length === 1) {
  7. console.log('single click')
  8. }
  9. else if(value.length === 2) {
  10. console.log('double click')
  11. }
  12. else if(value.length === 3) {
  13. console.log('triple click')
  14. }
  15. });

请使用如下的方式来阅读此段代码,要缓冲的流 clicks$ 每300毫秒会发出它的值,300毫秒是由 scissor$ 流决定的。所以 scissor$ 流是一把剪刀,如果你愿意,它可以将 clicks$ 流切碎,瞧,我们有一个优雅获取双击的方法了。如你所见,上面的代码捕获了所有类型的点击,但通过解除注释 filter() 操作的话,我们就只能获得双击和三连击。

filter() 操作符同样可以用于其它目的,像随着时间推移记录 UI 发生了什么并回放给用户,唯一能限制它所能做的就是你的想象力。