RxJS系列教程(九) 操作异步流

Rx,不管你是JS,Java,Python还是Swift,玩的就是操作符。每个操作符怎么用,官方文档写得不能再清楚了,再配上例子和图,您要还整不明白就……继续整,整到明白为止,量变肯定会导致质变,就像国足永远不争气,都是永恒的真理。

然而整明白了操作符是干啥的,确不知道在啥地方用,这就是cookbook或者recipe之类书籍有市场的原因。说白了就像中国的教育,理论都是大拿,实践都是newbie。

当然,概念还是要讲的,理论是基础,实践是真理。下面我们来看一个场景:一个web app,电脑上用,用鼠标,手机平板上用,用手指,这不废话么!然而懂行的人知道里面有奥妙,用鼠标的,会触发mousedown,mouseup,mousemove事件,用手指的,会触发touchstart,touchend,touchmove事件。抛出来的问题是,能不能只写一套逻辑供两套事件使用呢?当然能了,拷贝粘贴呗。我们来看看RxJS中怎么做。

const mouseupStream = Rx.Observable.fromEvent(document, 'mouseup');// 鼠标按键抬起事件流
const touchendStream = Rx.Observable.fromEvent(document, 'touchend');// 触摸屏幕抬起事件流

mouseupStream.subscribe(/*处理逻辑*/);
touchedStream.subscribe(/*处理逻辑*/);

这不和没用RxJS一个意思么。别着急啊,没说这是最佳解决方案啊,接着往下看。

这两个事件是非常类似的,既然类似,我们能不能把它们当成一样的事件,合并起来呢?答案是,必须可以!

RxJS中的merge()操作符就是用来合并两个流的,既然是两个,就会有顺序问题,如果是同步操作,那就是有序的;如果是异步操作,merge()操作符内部会根据时间来做决定,合并起来的流中的事件就是无序的,交叉出现的。这里说句题外话,其实将操作符都要配图的,但是我实在是懒,况且图也不是我画的,大家就自己上网站上看吧。

merge()既有静态方法实现也有实例方法实现,以后讲操作符不额外声明的话,默认都有两种实现。

Rx.Observable.merge(mouseupStream, touchendStream);
//或
mouseupStream.merge(touchendStream);

假如我们最终的需求是要鼠标点击或手指触摸位置的数据,请看代码:

Rx.Observable.merge(mouseupStream, touchendStream)
  .do(event => console.log(event.type))// debug,查看事件类型
  .map(event => {
    switch (event.type) {
      case 'touchend':
        return {
          left: event.changedTouches[0].clientX,
          top: event.changedTouches[0].clientY
        };
      case 'mouseup':
        return {
          left: event.clientX,
          top: event.clientY
        }
    }
  })
  .subscribe(object => {
    console.log(`位置坐标为:(${object.left}, ${object.top})`);
  })

我们知道Rx一脉相承自函数式编程,那这段代码就有点说不过去了,怎能出现命令式的控制语句呢,说的就是你switch!当然理想化的东西能不能实现还是一回事儿呢,况且规则是人定的。但我们尽量在操作符中不出现命令式语句,把不得不出现的逻辑推迟到observer端来处理。我在系列三中提到过副作用也都放到observer来处理。

当然更好的方式是,我们在observable端就把数据处理好,observer接收时就不用再做处理了。

const pmouseupStream = mouseupStream.map(event => ({
  left: event.clientX,
  top: event.clientY
}));

const ptouchendStream = touchendStream.map(event => ({
  left: event.changedTouches[0].clientX,
  top: event.changedTouches[0].clientY
}));

Rx.Observable.merge(pmouseupStream, ptouchendStream)
  .subscribe(object => {
    console.log(`位置坐标为:(${object.left}, ${object.top})`);
  });

完美!

没有任何事会一直完美下去的。如果我们想要两个异步流合并后保持先后顺序呢?没问题,concat()操作符完美解决你的问题。这个没啥可讲的,同步流能用吗?您自己试试吧,实践才是真理。

继续继续。刚才命令式的switch被我们痛骂了一顿,RxJS中的switch()可就是个宝儿喽。叫李嘉诚的千千万,然而首富就那么一个,这就是差距。

switch()操作符只有实例方法实现方式。它的作用是切换到最新的那个observable。这到底是啥意思呢,请看图:

switch操作符

当我们第一次点击按钮的时候,map中的函数返回值又是一个observable,也就是说,switch接收到的是一个内嵌observable的observable,这时候switch会用内嵌的observable取代外层的那个observable,也就是click事件流被新产生的时间事件流取代了。

当我们在第二次点击按钮的时候(这个时候上个时间事件流还没有结束),又会产生一个新的时间事件流,这个新的时间事件流不仅取代了click事件流,还取代了第一次点击按钮产生的那个时间事件流。谨记switch永远会切换到最新的那个事件流。

内嵌observable引出了merge(),concat(),switch()各自的兄弟:mergeMap(),concatMap(),switchMap()。

mergeMap

还用上面的例子,把map改成mergeMap,去掉switch,区别是,click事件流同样被取代,但第一次点击产生的时间事件流不会被第二次点击的时间事件流取代,而是合并成了一个流(无序)。

switchMap

这个操作符完成的事儿实际就是上面例子中的map+switch。

concatMap

用concatMap替换map,去掉switch,点击三次按钮,我们会看到控制台输出三次0到4,前一次不结束,后面的一直等待。这里给大家一个赞赏我的机会,请用三个鼠标事件流+concatMap操作符+takeUntil操作符完成拖放页面元素的功能。你会发现,哇~好简单好明了。

takeUntil操作符接收一个observable为参数,含义是,接收上游事件并让它通过,直到参数observable开始发送事件。

其实更直观的感受这些操作符的强大之处,或者说Rx的强大之处,应该用ajax、promise这些更贴近日常开发的例子,譬如说之前提到过的搜索框提示,或者监控股票价格,气象温度等等,就留个各位自己尝试吧,实践出真知嘛。

你可能感兴趣的:(RxJS系列教程(九) 操作异步流)