30 天精通 RxJS(18): Observable Operators - switchMap, mergeMap, concatMap

今天咱們要講三個很是重要的 operators,這三個 operators 在不少的 RxJS 相關的 library 的使用示例上都會看到。不少初學者在使用這些 library 時,看到這三個 operators 極可能就放棄了,但其實若是有把這個系列的文章完整看過的話,如今應該就能很好接受跟理解。git

Operators

concatMap

concatMap 其實就是 map 加上 concatAll 的簡化寫法,咱們直接來看一個示例github

var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .map(e => Rx.Observable.interval(1000).take(3))
                .concatAll();

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

上面這個示例就能夠簡化成json

var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .concatMap(
                    e => Rx.Observable.interval(100).take(3)
                );

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

先後兩個行爲是一致的,記得 concatMap 也會先處理前一個送出的 observable 在處理下一個 observable,畫成 Marble Diagram 以下promise

source : -----------c--c------------------...
        concatMap(c => Rx.Observable.interval(100).take(3))
example: -------------0-1-2-0-1-2---------...複製代碼

這樣的行爲也很常被用在發送 request 以下瀏覽器

function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.concatMap(
                    e => Rx.Observable.from(getPostData()));

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

JSBin | JSFiddlebash

這裏咱們每點擊一下畫面就會送出一個 HTTP request,若是咱們快速的連續點擊,你們能夠在開發者工具的 network 看到每一個 request 是等到前一個 request 完成纔會送出下一個 request,以下圖ide

這裏建議把網速模擬調到最慢工具

從 network 的圖形能夠看得出來,第二個 request 的發送時間是接在第一個 request 以後的,咱們能夠確保每個 request 會等前一個 request 完成才作處理。post

concatMap 還有第二個參數是一個 selector callback,這個 callback 會傳入四個參數,分別是fetch

  1. 外部 observable 送出的元素
  2. 內部 observable 送出的元素
  3. 外部 observable 送出元素的 index
  4. 內部 observable 送出元素的 index

回傳值咱們想要的值,示例以下

function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.concatMap(
                e => Rx.Observable.from(getPostData()), 
                (e, res, eIndex, resIndex) => res.title);

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

JSBin | JSFiddle

這個示例的外部 observable 送出的元素就是 click event 實例,內部 observable 送出的元素就是 response 實例,這裏咱們回傳 response 實例的 title 屬性,這樣一來咱們就能夠直接收到 title,這個方法很適合用在 response 要選取的值跟前一個事件或順位(index)相關時。

switchMap

switchMap 其實就是 map 加上 switch 簡化的寫法,以下

var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .map(e => Rx.Observable.interval(1000).take(3))
                .switch();

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

上面的代碼能夠簡化成

var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .switchMap(
                    e => Rx.Observable.interval(100).take(3)
                );

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

畫成 Marble Diagram 表示以下

source : -----------c--c-----------------...
        concatMap(c => Rx.Observable.interval(100).take(3))
example: -------------0--0-1-2-----------...複製代碼

只要注意一個重點 switchMap 會在下一個 observable 被送出後直接退訂前一個未處理完的 observable,這個部份的細節請看上一篇文章 switch 的部分。

另外咱們也能夠把 switchMap 用在發送 HTTP request

function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.switchMap(
                    e => Rx.Observable.from(getPostData()));

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

JSBin | JSFiddle

若是咱們快速的連續點擊五下,能夠在開發者工具的 network 看到每一個 request 會在點擊時發送,以下圖

灰色是瀏覽器原生地停頓行爲,實際上灰色的一開始就是 fetch 執行送出 request,只是卡在瀏覽器等待發送。

從上圖能夠看到,雖然咱們發送了多個 request 但最後真正印出來的 log 只會有一個,表明前面發送的 request 已經不會形成任何的 side-effect 了,這個很適合用在只看最後一次 request 的情境,好比說 自動完成(auto complete),咱們只須要顯示使用者最後一次打在畫面上的文字,來作建議選項而不用每一次的。

switchMap 跟 concatMap 同樣有第二個參數 selector callback 可用來回傳咱們要的值,這部分的行爲跟 concatMap 是同樣的,這裏就再也不贅述。

mergeMap

mergeMap 其實就是 map 加上 mergeAll 簡化的寫法,以下

var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .map(e => Rx.Observable.interval(1000).take(3))
                .mergeAll();

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

上面的代碼能夠簡化成

var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .mergeMap(
                    e => Rx.Observable.interval(100).take(3)
                );

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

畫成 Marble Diagram 表示

source : -----------c-c------------------...
        concatMap(c => Rx.Observable.interval(100).take(3))
example: -------------0-(10)-(21)-2----------...複製代碼

記得 mergeMap 能夠並行處理多個 observable,以這個例子來講當咱們快速點按兩下,元素髮送的時間點是有機會重疊的,這個部份的細節你們能夠看上一篇文章 merge 的部分。

另外咱們也能夠把 switchMap 用在發送 HTTP request

function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.mergeMap(
                    e => Rx.Observable.from(getPostData()));

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

JSBin | JSFiddle

若是咱們快速的連續點擊五下,你們能夠在開發者工具的 network 看到每一個 request 會在點擊時發送而且會 log 出五個實例,以下圖

mergeMap 也能傳入第二個參數 selector callback,這個 selector callback 跟 concatMap 第二個參數也是徹底同樣的,但 mergeMap 的重點是咱們能夠傳入第三個參數,來限制並行處理的數量

function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.mergeMap(
                e => Rx.Observable.from(getPostData()), 
                (e, res, eIndex, resIndex) => res.title, 3);

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

JSBin | JSFiddle

這裏咱們傳入 3 就能限制,HTTP request 最多隻能同時送出 3 個,而且要等其中一個完成在處理下一個,以下圖

你們能夠注意看上面這張圖,我連續點按了五下,但第四個 request 是在第一個完成後才送出的,這個很適合用在特殊的需求下,能夠限制同時發送的 request 數量。

RxJS 5 還保留了 mergeMap 的別名叫 flatMap,雖然官方文件上沒有,但這兩個方法是徹底同樣的。請參考這裏

switchMap, mergeMap, concatMap

這三個 operators 還有一個共同的特性,那就是這三個 operators 能夠把第一個參數所回傳的 promise 實例直接轉成 observable,這樣咱們就不用再用 Rx.Observable.from 轉一次,以下

function getPersonData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.concatMap(e => getPersonData());
                                    //直接回傳 promise 實例

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});複製代碼

至於在使用上要如何選擇這三個 operators? 其實都仍是看使用情境而定,這裏筆者簡單列一下大部分的使用情境

  • concatMap 用在能夠肯定內部的 observable 結束時間比外部 observable 發送時間來快的情境,而且不但願有任何並行處理行爲,適合少數要一次一次完成到底的的 UI 動畫或特別的 HTTP request 行爲。
  • switchMap 用在只要最後一次行爲的結果,適合絕大多數的使用情境。
  • mergeMap 用在並行處理多個 observable,適合須要並行處理的行爲,像是多個 I/O 的並行處理。

建議初學者不肯定選哪個時,使用 switchMap

在使用 concatAll 或 concatMap 時,請注意內部的 observable 必定要可以的結束,且外部的 observable 發送元素的速度不能比內部的 observable 結束時間快太多,否則會有 memory issues

今日小結

今天的文章內容主要講了三個 operators,若是有看完上一篇文章的讀者應該不難吸取,主要仍是使用情境上須要思考以及注意一些細節。

不知道今天讀者有沒有收穫呢? 若是有任何問題,歡迎留言給我,謝謝

相關文章
相關標籤/搜索