iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 19
8
Modern Web

30 天精通 RxJS系列 第 19

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

本篇文章搬家囉! 這裡不再回覆留言,請移至 https://blog.jerry-hong.com/series/rxjs/thirty-days-RxJS-18/


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

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

Operators

concatMap

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

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'); }
});

上面這個範例就可以簡化成

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 如下

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 | JSFiddle

這裡我們每點擊一下畫面就會送出一個 HTTP request,如果我們快速的連續點擊,大家可以在開發者工具的 network 看到每個 request 是等到前一個 request 完成才會送出下一個 request,如下圖

這裡建議把網速模擬調到最慢

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

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

  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 的部分。

另外我們也可以把 mergeMap 用在發送 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,如果有看完上一篇文章的讀者應該不難吸收,主要還是使用情境上需要思考以及注意一些細節。

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


上一篇
30 天精通 RxJS(17): Observable Operators - switch, mergeAll, concatAll
下一篇
30 天精通 RxJS(19): 實務範例 - 簡易 Auto Complete 實作
系列文
30 天精通 RxJS30
3
法蘭克
iT邦新手 5 級 ‧ 2017-01-04 16:28:37

想額外請問一下,是什麼樣的情況會開始接觸到不同的pattern 或是筆者在什麼情況開始去學習比較不同pattern對於自己應用上的差異?

包含整個系列所提的observable, 或是functional programming vs object oriented 等等等討論,目前處在好像知道在幹嘛,好像說得出來一些優缺點,但其實什麼都不太了解的狀況。
(對於一個從前端開始入門碰到的問題,想尋求意見)

感謝

這裡有兩個問題 問題也都蠻大的 可以寫成兩篇文章來回答 XD

我先回答第一個問題

這是我自己的一些學習歷程跟心得

第一個階段 前端入門

其實不用在意什麼 Design Pattern,甚至是 Programming Paradigm(FP, OOP) 也不用管,只要能把功能做出來完成需求,會用一些套件就很棒了。

在這個階段,你會一點點的 HTML、CSS、JS,要你修改現有專案的一些小功能是沒有問題的,但其實你常常是搞不清楚真正的運作原理,有時候是瞎貓碰到死耗子解出來的,程式碼也很有可能在一段時間後被你用的很亂。

第二個階段 前端上手

在這個階段,你已經開始深入的學習 HTML、CSS 跟 JS,這三個基本的核心觀念你都已經具備,雖然沒有到滾瓜爛熟,也沒有非常的全面,但已經能夠理解絕大部分的程式碼,只是有時候還是會卡住,但只要再想想或是查資料就能夠解決,而且絕大多數的 Library 你一看就會知道要如何使用,並且開始對自己寫的程式碼有所要求。

第三個階段 熟練前端

在這個階段,你會使用一些前端工具,並且熟悉某個前端框架,能夠透過工具來建立純前端的專案,有能力獨立完成 SPA 的網站。

在這個時候,你會開始去學習如何讓自己的程式碼更加的簡潔,工作如何更有效率,開始嘗試看某些 Library 或 Framework 的 source code,雖然可能不是每次都能全部看懂,但有已經可以理解 6~7 成。

通常會在這個階段開始去研究各種 Design Pattern 還有 Programming Paradigm,但都只是剛開始應用,不會有很深的理解。

第四個階段 精通前端

在這個階段,你很可能已經會了兩個以上的前端框架,並且有能力自己從無到有的建立前端專案,包含工作流程的自動化、各種前端工具的設定,以及自動化測試的撰寫。也對各個前端框架有所掌握,就算自己沒有用過,但也能透過文章或教學,快速理解其運作的原理,並且你同時也熟悉一個後端語言。

你也會在這個階段對各種 Design Pattern 有比較深入的體會,對不同的 Programming Paradigm 也能用程式碼清楚的表達。

第五個階段 前端大師

這個階段,筆者也還沒到達只是自己的想像跟目標

在這個階段,你能用白話的方式清楚地闡述各種觀念,能很輕鬆的教別人學習前端甚至跟完全不懂技術的人有良好的溝通,並且對前端各種技術都有一定深度的認識,像是 SVG, Canvas, IndexDB, WebGL, Web Socket, Service Worker... 等。

總結

這是我自己在學習前端過程區分的五個階段,大概會在第三到第四個階段開始對 Design Pattern 有所研究,如果你還沒到第三個階段,Design Pattern 請看看就好,讓自己先打好 HTML, CSS, JS 的基礎,並且找一個 framework 來學,並且實際用在專案上,在這個過程當中很自然的就會碰到 Design Pattern。

第二個問題應該是 不太清楚 FP 跟 OOP 還有 Observable

這邊要先釐清

Observable 可以說是一種 Design Pattern (只是沒有出現在四人幫的書裡)
FP 跟 OOP 則是 Programming Paradigm

其實 FP 跟 OOP 這兩個並不相斥,他們是可以同時使用並且能夠很好的搭配在一起,所以不用擔心說是不是用了一個就要小心不要參雜了另一個。

而 FP 跟 OOP 重要的是觀念跟思想,他們主要做的是告訴我們應該如何撰寫程式,但這並沒有任何強制的規定,不是說我們一定要如何寫程式才能夠稱為 OOP 或 FP;而是我們要能夠用 OOP 或 FP 的思想來思考如何撰寫程式碼才能讓程式碼更好。

舉個例子 像是 FP 所說的 pure function。

平常我們在寫 function 的時候就可以想,要如何盡可能的不要有 side-effect,運算結果不要受外部變數影響,不要修改傳入變數的值。

利用這些想法來組織我們的程式碼,但不是說每個 function 你都要嚴格遵守,像是 side-effect 就是一定會存在在某些 function 裡頭,但你可以試試把具有 side-effect 的 function 跟其他 function 分開。

永遠記得 他們的存在只是讓你在撰寫程式時,能夠利用他們的想法來寫出更好的程式碼。

至於 Observable 如果覺得沒有很懂,可以把前面的文章再看一下,但最重要的是動手寫寫看,完成幾個小需求,然後去睡覺過幾天就會理解了。 (去睡覺不是我亂說的,是認知心理學講的,人類大腦中有很多神經元,理解一個知識的過程其實就是神經元長出新的觸手,而長出新的觸手是需要時間跟休息的)

如果還是有困惑歡迎留言給我喔!

哇感謝超棒的回覆,有比較理解了也解惑了
超用心希望你得獎哈哈哈

1
法蘭克
iT邦新手 5 級 ‧ 2017-01-04 16:47:14

阿然後關於這篇的問題:
selector callback,這個 callback 會傳入四個參數

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

1,2了解了 想問3,4所指的index 是什麼意思 ?

index 是指 送出的第幾個元素 從 0 開始
跟陣列是一樣的概念

ok/images/emoticon/emoticon37.gif

0
huli
iT邦新手 5 級 ‧ 2017-12-02 23:47:54

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

另外我們也可以把 switchMap 用在發送 HTTP request

這邊應該是 「另外我們也可以把 mergeMap 用在發送 HTTP request」

感謝
被發現用 copy&paste 了 XD

0
JerryHong
iT邦新手 5 級 ‧ 2019-05-09 15:22:50

本篇文章搬家囉! 這裡不再回覆留言,請移至 https://blog.jerry-hong.com/series/rxjs/thirty-days-RxJS-18/

我要留言

立即登入留言