不要把Rx用成Promise

不要把Rx用成Promise

Promise这个概念在JS开发者里可谓是深入人心,从最早的Promise/A提出,到后来ES6规范的发布,Promise已经到处开花了,使用者主要是用它来避免callback hell,而后配合generate function,更是可以玩出这样的花样:
'use strict';
const next = (gen,val) => {
     const n = gen.next(val);
     !n.done && Promise.resolve(n.value).then(d=>next(gen,d));
 }
const run = genfunc => next(genfunc());
const sleep = time=>new Promise(resolve=>setTimeout(resolve,time));
run(function* (){
    for(let i=0;i<10;i++){
        let ret = yield fetch("/url/"+i).then(res=>res.text());
        console.log(ret);
        yield sleep(1000);
    } 
})

写起来很清晰,而且ES2016可能有的async关键字也是依赖于Promise,所以Promise已经成了现代JS开发必须要用的特性了。

而在其他环境下,比如说android/ios开发的情景下,同样是有大量的callback回调,因为传统app开发无可避免的涉及到 响应事件/网络请求/定时器等等需要异步回调的需求,为了避免callback hell,所以业界也很快接受了Rx这种模式,android那边有RxJava/RxAndroid,ios这边有RxSwift等。

比如说在android上,使用RxJava进行网络请求

@OnClick(R.id.btn)
void onClick(){
    getApi().
        .subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(result->{
            // do something
        })
}

粗略一看,大概会以为它是另外一种形式的Promise,同样是避免callback hell,同样是链式调用,然而事实上Promise和Rx是两种截然不同的模式,不能混为一谈。

1. Promise顾名思义,提供的是一个允诺,这个允诺就是在调用then之后,它会在未来某个时间段把异步得到的result/error传给then里的函数。

Rx不是允诺,它本质上还是由订阅/发布模式引出来的,它的核心思想就是数据响应式,源头是数据产生者,经过一系列的变换/过滤/合并的操作,被数据消费者所使用,数据消费者何时响应,完全取决于数据流何时能流下来。

2. Promise需要调用then或者catch才能够执行,catch是另一种形式的then,调用then或者catch之后,它返回一个新的Promise,这样新的Promise也可以同样被调用,所以可以做成无限的then链。

Rx的数据是否流出不取决于是否subscribe,也就是说一个observable在未被订阅的时候也可以流出数据,在之后它被订阅过后,先前的数据是无法被数据消费者所查知,所以Rx还引入了一个lazy模式,允许数据缓存着直到被subscribe,但是数据是否流出还是并不依赖subscribe。

Rx的observable被subscribe之后,并不是继续返回一个新的observable,而是返回一个subscriber,这样用来取消订阅,但是这也导致了链式断裂,所以它不能像Promise那样组成无限then链。

3. Promise的数据是一次性流出的,因为Promise内部维持着状态,初始化的pending,转成resolved或者rejected之后,状态就不可逆转了。

举例说promise().then(A).then(B).then(C).catch(D),数据是顺着链以此传播,但是只有一次,数据从A到B之后,A这个promise的状态发生了改变,从pedding转成了resolved,那么它就不可能再产生内容了,所以这个promise已经不是活动性的了。

而Rx则不同,我们从Rx的接口就可以知道,它有onNext,onComplete和onError,onNext可以响应无数次,这也是符合我们对数据响应式的理解,数据在源头被隔三差五的发出,只要源头认为没有流尽(onComplete)或者出了问题(onError),那么数据就可以不断的流到响应者那边。

举例来说,我们响应一个按钮的点击事件,那么我们可以把这个事件抽象为一个数据流,只要按钮还在,那么我们就认为它是可以产生数据的。

 Rx.Observable.fromEvent(btn, 'click')
   .map(() => input.value)
   .filter(text => !!text)
   .distinctUntilChanged()
   .flatMapLatest(Rx.Observable.fromPromise(fetch("/").then(res=>res.text()))
   .subscribe(value=>{
      input.value = value;
   });

再比如说定时器,我们可以把一个每三秒执行一项复杂的操作抽象成源头是定时器的操作,比如这样

Rx.Observable.timer(0,3000)
             .timeInterval()
             .flatMapLatest()
             .subscrbe(value=>{
             })
在这里面,数据是流动的,是活的,而不是像Promise那样交给下一个then之后,自己就死了,这种差别需要格外注意。

4. Promise用then链来处理数据,包括对数据进行过滤、合并、变换等操作,它没有真正意义上的数据消费者,then链的每一个都是数据消费者,所以它非常适合组装流程式,也就是说A完成之后做B,然后B完成后去完成C这种流程,这些流程可以无穷无尽,没有底的。

Rx有数据产生的源头和严格意义的数据消费者,数据可以在中间的操作符里被处理,比如说做过滤,做合并,做节流,变换成新的数据源头等等,可以把它想象成一个完整的数据链,有头也有尾,到了最终消费者那边这个数据流就算到底。

5. Promise的状态发生改变后,我们如果再想重新从源头开始的话,就需要在后续then链递归最开始的那一步,因为后续者是新的promise,无法感知源头的那个Promise。

而Rx的observable可以感知源头,它有类似于retry、repeat这种重新开始的运算符,我们可以很方便的链式调用它,而不需要封装成函数再递归。

6. Promise的then链里面,每一行都是同样的角色,也就是Promise,所以它既可以是源头,也可以是数据处理者。

Rx这边的observable还有一些变种,比如说常用的subject,它可以充当双面角色,可以订阅也可以发消息,这样的话我们还可以用它来做很多封装的工作。

所以Promise和Rx这两个模式的思想差别很清晰,一个是流程式,一个是数据响应式,Promise可以用来贯串一连串单一的流程,而且这个流程是可以无限的,而Rx是用一个数据流来贯串所有操作符,它有一个真正意义上的数据消费者。

我们在哪些场景下用Rx比较方便?首先是需要源源不断的流出数据的场景,因为Promise是一次性的,不适合做这类工作。

比如说把事件/定时器抽象成Rx的Observable更合适,事件可以响应很多次,定时器也可以响应很多次,我们还可以利用Rx的debounce运算符来进行节流,在频繁触发事件的时候过滤那些重复的。

其次是可能需要重试的场景,由于Rx有retry或者repeat这种从源头开始的运算符,我们可以用它来执行比如“出错后重试三次”之类动作,而Promise就需要你递归处理了,破坏了then的链式。

而Promise也有一些优于Rx的场景,比如最开始我们举例的那个结合generate function做的yield自动调用,Promise的链式是无穷的,所以适合这类流程式的工作,async关键字依赖Promise也是正确之举。

还有就是那些一次性的工作,比如说我们请求http api,在得到数据之后response socket就会关闭,这个时候不会有第二次的数据流动,这就是一次性的数据流动,Promise就可以完成的很好。

这两种模式都有自己的想法,所以在使用Rx的时候,不要把它当成Promise来用,记住它的本质是数据响应。

编辑于 2016-02-06

文章被以下专栏收录