从高阶Observable 的概念与RxJS Map 操作符来介绍四大让人却步的Map 系列操作符— MergeMap
、ConcatMap
、SwitchMap
以及ExhaustMap
。先从简单的Map 操作符开始,以下面的程式码举例:
Rx.Observable
.interval(1000)
.take(5)
.map(item => item + 1)
.subscribe(result => console.log(result));
Interval
操作符做的事情是— 从零开始,每隔一段时间输出递增的值出来,跟 setInterval
做的事情很像,只是 interval
就是一种计时器。而 take
操作符会取得前面几项的值,以上面的程式码为例的话,计时器从0 输出到4 的时候就会结束。而 map
操作符做的事情很简单,就是对来源的数值做某种程序的转换,因此,上面的程式码,计数器从0 数到4,然后每个值经过 map
操作符之后得到的结果都会加一,时间轴图可以以下面的图来表示:
但如果今天我们将程式码,里面的Map 操作符也转换成Observable 型别输出:
Rx.Observable
.interval(1000)
.take(5)
.map(item =>
Rx.Observable
.of(item + 1)
.delay((item + 1) * 1000) // 延迟操作符
)
.subscribe(result => console.log(result));
上面的程式码输出的结果不会纯粹只是1 ~ 5 这样的结果,而是五个Observable 物件,跟我们想像中的不太一样,因为Observable 一定要被 subscribe
过后才会运作。因此,我们需要把 subscribe
内部的 result
再做一次 subscribe
的动作:
Rx.Observable
.interval(1000)
.take(5)
.map(item =>
Rx.Observable
.of(item + 1)
.delay((item + 1) * 1000) // 延迟操作符
)
.subscribe(observable => {
/* 内部再做一次subscribe 的动作*/
observable.subscribe(item => console.log(item));
} );
因为里面我刻意再加一个延迟操作符(delay),因此我们直接以视觉化的方式把时间轴图先丢出来,结果如下:
也就是说,计时器会 map
出新的时间轴,第一秒生出的时间轴会延迟一秒钟然后输出1 这个数值;第二秒生出的时间轴会延迟两秒钟然后输出2 这个数值,依此类推。而其中,我们使用 map
操作符输出第二层Observable 的行为,很像二维时间轴展开,因此在第一层作 subscribe
的动作的时候,需要在内部做第二层的subscribe
,这就是高阶Observable 的概念。
再来,刚刚的范例看起来很像是把所有的第二维度的时间轴再做摊平的动作(搜集所有的延迟过后的时间轴数值回归到一维时间轴),我们可以将刚刚的行为使用 mergeAll
操作符来简化:
Rx.Observable
.interval(1000)
.take(5)
.map(item =>
Rx.Observable
.of(item + 1)
.delay((item + 1) * 1000) // 延迟操作符
)
.mergeAll()
.subscribe(item => console.log(item) );
然后因为这个Pattern 实在太常被使用了,因此可以再次简化为mergeMap
:
Rx.Observable
.interval(1000)
.take(5)
.mergeMap(item =>
Rx.Observable
.of(item + 1)
.delay((item + 1) * 1000) // 延迟操作符
)
.subscribe(item => console.log(item) );
其实拖曳事件即是以 mergeMap
操作符做出来的应用,拖曳事件就是借助高阶Observable 的概念时做出来的:
mouseDown$
.mergeMap(() => mouseMove$.takeUntil(mouseUp$))
.subscribe(() => console.log('dragging...'))
因此,mergeMap
就是将二维的时间轴触发出来的结果全部都摊平到一维的时间轴(其实也可以说是一种OR 逻辑的概念)。另外,如果我们改成 concatMap
会发生什么事?(请看下面的程式码与时间轴图)
Rx.Observable
.interval(1000)
.take(5)
.concatMap(item =>
Rx.Observable
.of(item + 1)
.delay((item + 1) * 1000) // 延迟操作符
)
.subscribe(item => console.log(item) );
你可以发现,第一秒跟第二秒产生的二维时间轴输出的结果差不多,但是第三秒输出的二维时间轴串接*在第二秒二维时间轴结束之后*,第四秒输出之二维时间轴串接在第三秒二维时间轴结束之后,依此类推。因此可以得出,concatMap
具有列队或者是串接的概念(Concatenation)。
再来登场的是switchMap
,试着根据下面的范例程式码与时间轴图推理看看 switchMap
的性质:
Rx.Observable
.interval(1000)
.take(5)
.switchMap(item =>
Rx.Observable
.of(item + 1)
.delay((item + 1) * 1000) // 延迟操作符
)
.subscribe(item => console.log(item) );
Switch
具有切换的概念,可以看到虽然第一秒产生时间轴可以顺利输出,但是第二秒产生的时间轴,因为需要Delay 两秒钟的时间,可是在还没结束的时候,第三秒的时间轴被展开了,于是焦点从第二个时间轴切换到第三个时间轴,依此类推,直到最后一个时间轴因为没有任何其他的时间轴展开,因此可以安然地在五秒钟过后输出结果。
通常 switchMap
常用在送请求的状况,比如说如果是做使用者搜寻的功能,我们可能会绑定 input
事件,随着使用者输入文字,然后送出请求。可是因为有可能使用者打字速度很快,所以可能一隙之间会送出很多个请求,但我们想要的是最新的请求送出回传之结果,因此我们可以藉由 switchMap
帮我们实现这个功能。最简单的雏形如下:
input$
.switchMap((e) => fetchSearchResult(e.target.value))
.subscribe(results => console.log(results));
最后是exhaustMap
,一样可以根据下面的程式码与时间轴图来推理看看 exhaustMap
的性质:
Rx.Observable
.interval(1000)
.take(5)
.exhaustMap(item =>
Rx.Observable
.of(item + 1)
.delay((item + 1) * 1000) // 延迟操作符
)
.subscribe(item => console.log(item) );
exhaustMap
的性质,我不太确定要怎么翻译,因为我有听过它是有所谓的映射的性质(不过不太知道意思),但我是把它解读成“霸道” 操作符,当二维时间轴被展开的时候,如果该时间轴还未终结的话,其他准备要展开的时间轴会自动被忽略掉(或耗竭掉Exhausted)。上面的图显示出当第二秒时间轴被展开,第三秒的时间因为还有时间轴在delay,因此被无条件忽略掉。而在第四秒的时候,因为第二秒的时间轴结束了,所以可以顺利被展开。
至于为何会被我解读成霸道操作符则是因为任何第二维时间轴被成功展开的时候会霸占岗位,其他想要展开的时间轴没办法挤上去,于是必须得等到霸占的时间轴结束之后才能够空出空位让其他的时间轴展出去。而且这个例子还蛮神奇的地方是,如果你把 take
操作符拿掉,它会展开成一种指数型的计数器(1, 2, 4, 8, 16 …)。
以上就是高阶Observable 的概念而言生出的四大Map 相关操作符的介绍。